queueio 0.6.0__tar.gz → 0.7.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 (112) hide show
  1. {queueio-0.6.0 → queueio-0.7.0}/CHANGELOG.md +21 -1
  2. {queueio-0.6.0 → queueio-0.7.0}/PKG-INFO +7 -5
  3. {queueio-0.6.0 → queueio-0.7.0}/README.md +4 -4
  4. {queueio-0.6.0 → queueio-0.7.0}/mkdocs.yml +1 -1
  5. {queueio-0.6.0 → queueio-0.7.0}/pyproject.toml +5 -2
  6. queueio-0.7.0/pyrightconfig.json +5 -0
  7. {queueio-0.6.0 → queueio-0.7.0}/queueio/__init__.py +2 -2
  8. {queueio-0.6.0 → queueio-0.7.0}/queueio/__main__.py +26 -47
  9. queueio-0.7.0/queueio/backend.py +18 -0
  10. {queueio-0.6.0 → queueio-0.7.0}/queueio/broker.py +7 -13
  11. {queueio-0.6.0 → queueio-0.7.0}/queueio/broker_test.py +9 -16
  12. {queueio-0.6.0 → queueio-0.7.0}/queueio/freethreading_test.py +5 -1
  13. {queueio-0.6.0 → queueio-0.7.0}/queueio/journal.py +0 -6
  14. {queueio-0.6.0 → queueio-0.7.0}/queueio/monitor.py +2 -2
  15. queueio-0.7.0/queueio/pika/__init__.py +31 -0
  16. {queueio-0.6.0 → queueio-0.7.0}/queueio/pika/broker.py +27 -23
  17. queueio-0.7.0/queueio/pika/broker_test.py +18 -0
  18. {queueio-0.6.0 → queueio-0.7.0}/queueio/pika/journal.py +13 -12
  19. queueio-0.7.0/queueio/pika/journal_test.py +15 -0
  20. {queueio-0.6.0 → queueio-0.7.0}/queueio/pika/receiver.py +1 -0
  21. queueio-0.7.0/queueio/psycopg/README.md +136 -0
  22. queueio-0.7.0/queueio/psycopg/broker-queries.md +129 -0
  23. queueio-0.7.0/queueio/psycopg/journal-queries.md +172 -0
  24. queueio-0.7.0/queueio/psycopg/journal.py +97 -0
  25. queueio-0.7.0/queueio/psycopg/journal_test.py +40 -0
  26. queueio-0.7.0/queueio/psycopg/receiver-queries.md +254 -0
  27. {queueio-0.6.0 → queueio-0.7.0}/queueio/queueio.py +49 -40
  28. queueio-0.7.0/queueio/queueio_test.py +309 -0
  29. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/basic_test.py +2 -4
  30. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/django_test.py +1 -1
  31. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/expanded_test.py +2 -4
  32. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/priority_test.py +2 -4
  33. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/queuevar_test.py +2 -4
  34. queueio-0.7.0/queueio/stub/__init__.py +21 -0
  35. {queueio-0.6.0 → queueio-0.7.0}/queueio/stub/broker.py +19 -12
  36. queueio-0.7.0/queueio/stub/broker_test.py +15 -0
  37. {queueio-0.6.0 → queueio-0.7.0}/queueio/stub/journal.py +11 -4
  38. queueio-0.7.0/queueio/stub/journal_test.py +12 -0
  39. queueio-0.6.0/pyrightconfig.json +0 -4
  40. queueio-0.6.0/queueio/pika/broker_test.py +0 -23
  41. queueio-0.6.0/queueio/pika/journal_test.py +0 -20
  42. queueio-0.6.0/queueio/queueio_test.py +0 -558
  43. queueio-0.6.0/queueio/stub/__init__.py +0 -0
  44. queueio-0.6.0/queueio/stub/broker_test.py +0 -22
  45. queueio-0.6.0/queueio/stub/journal_test.py +0 -19
  46. {queueio-0.6.0 → queueio-0.7.0}/.devcontainer/devcontainer.json +0 -0
  47. {queueio-0.6.0 → queueio-0.7.0}/.devcontainer/docker-compose.yml +0 -0
  48. {queueio-0.6.0 → queueio-0.7.0}/.github/workflows/build.yml +0 -0
  49. {queueio-0.6.0 → queueio-0.7.0}/.github/workflows/docs.yml +0 -0
  50. {queueio-0.6.0 → queueio-0.7.0}/.github/workflows/publish.yml +0 -0
  51. {queueio-0.6.0 → queueio-0.7.0}/.gitignore +0 -0
  52. {queueio-0.6.0 → queueio-0.7.0}/CNAME +0 -0
  53. {queueio-0.6.0 → queueio-0.7.0}/LICENSE +0 -0
  54. {queueio-0.6.0 → queueio-0.7.0}/dill.pyi +0 -0
  55. {queueio-0.6.0 → queueio-0.7.0}/docs_overrides/assets/images/favicon.svg +0 -0
  56. {queueio-0.6.0 → queueio-0.7.0}/docs_overrides/assets/images/logo.svg +0 -0
  57. {queueio-0.6.0 → queueio-0.7.0}/docs_overrides/assets/stylesheets/extra.css +0 -0
  58. {queueio-0.6.0 → queueio-0.7.0}/docs_overrides/partials/footer.html +0 -0
  59. {queueio-0.6.0 → queueio-0.7.0}/docs_overrides/partials/javascripts/palette.html +0 -0
  60. {queueio-0.6.0 → queueio-0.7.0}/logo.svg +0 -0
  61. {queueio-0.6.0 → queueio-0.7.0}/philosophy.md +0 -0
  62. {queueio-0.6.0 → queueio-0.7.0}/queueio/conftest.py +0 -0
  63. {queueio-0.6.0 → queueio-0.7.0}/queueio/consumer.py +0 -0
  64. {queueio-0.6.0 → queueio-0.7.0}/queueio/continuation.py +0 -0
  65. /queueio-0.6.0/queueio/django.md → /queueio-0.7.0/queueio/django/README.md +0 -0
  66. {queueio-0.6.0 → queueio-0.7.0}/queueio/django/__init__.py +0 -0
  67. {queueio-0.6.0 → queueio-0.7.0}/queueio/django/apps.py +0 -0
  68. {queueio-0.6.0 → queueio-0.7.0}/queueio/django/djp.py +0 -0
  69. {queueio-0.6.0 → queueio-0.7.0}/queueio/django/management/__init__.py +0 -0
  70. {queueio-0.6.0 → queueio-0.7.0}/queueio/django/management/commands/__init__.py +0 -0
  71. {queueio-0.6.0 → queueio-0.7.0}/queueio/django/management/commands/runserver.py +0 -0
  72. {queueio-0.6.0 → queueio-0.7.0}/queueio/django/management/commands/runserver_plus.py +0 -0
  73. {queueio-0.6.0 → queueio-0.7.0}/queueio/django/runserver_test.py +0 -0
  74. {queueio-0.6.0 → queueio-0.7.0}/queueio/event.py +0 -0
  75. {queueio-0.6.0 → queueio-0.7.0}/queueio/gather.py +0 -0
  76. {queueio-0.6.0 → queueio-0.7.0}/queueio/id.py +0 -0
  77. {queueio-0.6.0 → queueio-0.7.0}/queueio/invocation.md +0 -0
  78. {queueio-0.6.0 → queueio-0.7.0}/queueio/invocation.py +0 -0
  79. {queueio-0.6.0 → queueio-0.7.0}/queueio/journal_test.py +0 -0
  80. {queueio-0.6.0 → queueio-0.7.0}/queueio/message.py +0 -0
  81. {queueio-0.6.0 → queueio-0.7.0}/queueio/pause.py +0 -0
  82. {queueio-0.6.0 → queueio-0.7.0}/queueio/pika/threadsafe.py +0 -0
  83. {queueio-0.6.0/queueio/pika → queueio-0.7.0/queueio/psycopg}/__init__.py +0 -0
  84. {queueio-0.6.0 → queueio-0.7.0}/queueio/queue.py +0 -0
  85. {queueio-0.6.0 → queueio-0.7.0}/queueio/queue_test.py +0 -0
  86. {queueio-0.6.0 → queueio-0.7.0}/queueio/queuespec.md +0 -0
  87. {queueio-0.6.0 → queueio-0.7.0}/queueio/queuespec.py +0 -0
  88. {queueio-0.6.0 → queueio-0.7.0}/queueio/queuespec_test.py +0 -0
  89. {queueio-0.6.0 → queueio-0.7.0}/queueio/queuevar.md +0 -0
  90. {queueio-0.6.0 → queueio-0.7.0}/queueio/queuevar.py +0 -0
  91. {queueio-0.6.0 → queueio-0.7.0}/queueio/receiver.py +0 -0
  92. {queueio-0.6.0 → queueio-0.7.0}/queueio/registry.py +0 -0
  93. {queueio-0.6.0 → queueio-0.7.0}/queueio/result.py +0 -0
  94. {queueio-0.6.0 → queueio-0.7.0}/queueio/routine.py +0 -0
  95. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/__init__.py +0 -0
  96. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/basic.py +0 -0
  97. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/django/__init__.py +0 -0
  98. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/django/apps.py +0 -0
  99. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/django/job.py +0 -0
  100. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/django/models.py +0 -0
  101. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/django/settings.py +0 -0
  102. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/django/settings_djp.py +0 -0
  103. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/django/urls.py +0 -0
  104. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/expanded.py +0 -0
  105. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/priority.py +0 -0
  106. {queueio-0.6.0 → queueio-0.7.0}/queueio/samples/queuevar.py +0 -0
  107. {queueio-0.6.0 → queueio-0.7.0}/queueio/select.py +0 -0
  108. {queueio-0.6.0 → queueio-0.7.0}/queueio/stream.py +0 -0
  109. {queueio-0.6.0 → queueio-0.7.0}/queueio/stub/receiver.py +0 -0
  110. {queueio-0.6.0 → queueio-0.7.0}/queueio/suspension.py +0 -0
  111. {queueio-0.6.0 → queueio-0.7.0}/queueio/thread.py +0 -0
  112. {queueio-0.6.0 → queueio-0.7.0}/queueio/worker.py +0 -0
@@ -8,6 +8,25 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com).
8
8
  [Unreleased]
9
9
  ------------
10
10
 
11
+ [0.7.0] - 2026-03-09
12
+ --------------------
13
+
14
+ ### Breaking Changes
15
+
16
+ - The `pika` configuration key in `[tool.queueio]` has been renamed to `broker`.
17
+ - The `QUEUEIO_PIKA` environment variable has been renamed to `QUEUEIO_BROKER`.
18
+ - Removed `Broker.create(queue)`, in favor of `Broker.sync([queue])`.
19
+ - Removed `Broker.delete(queue)`, without replacement.
20
+ - Removed `QueueIO.create(queue)`, in favor of `QueueIO.sync([queue])`.
21
+ - Removed `QueueIO.delete(queue)`, without replacement.
22
+
23
+ ### Added
24
+
25
+ - Added `Broker.sync()` to back `queueio sync`.
26
+ - Added `QueueIO.sync()` to back `queueio sync`.
27
+ - `Backend` ABC with `broker()` and `journal()` methods
28
+ that return independent context managers with their own lifecycles.
29
+
11
30
  [0.6.0] - 2026-02-16
12
31
  --------------------
13
32
 
@@ -106,7 +125,8 @@ Thank you to Nick Anderegg for allowing me to use the queueio name for this proj
106
125
  - The queuespec syntax to `queue run` to consume multiple queues with shared capacity.
107
126
  - `queueio monitor` command to monitor activity in the queueio system.
108
127
 
109
- [Unreleased]: https://github.com/ryanhiebert/queueio/compare/tag/0.6.0...HEAD
128
+ [Unreleased]: https://github.com/ryanhiebert/queueio/compare/tag/0.7.0...HEAD
129
+ [0.7.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.6.0...tag/0.7.0
110
130
  [0.6.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.5.0...tag/0.6.0
111
131
  [0.5.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.4.0...tag/0.5.0
112
132
  [0.4.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.3.0...tag/0.4.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queueio
3
- Version: 0.6.0
3
+ Version: 0.7.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
@@ -21,6 +21,8 @@ Requires-Dist: typer>=0.15.4
21
21
  Provides-Extra: django
22
22
  Requires-Dist: django-cmd>=3.0; extra == 'django'
23
23
  Requires-Dist: django>=6.0; extra == 'django'
24
+ Provides-Extra: psycopg
25
+ Requires-Dist: psycopg>=3.3; extra == 'psycopg'
24
26
  Description-Content-Type: text/markdown
25
27
 
26
28
  ![queueio](https://raw.githubusercontent.com/ryanhiebert/queueio/main/logo.svg)
@@ -71,17 +73,17 @@ Add the configuration to your `pyproject.toml`:
71
73
 
72
74
  ```toml
73
75
  [tool.queueio]
74
- # Configure RabbitMQ
75
- pika = "amqp://guest:guest@localhost:5672/"
76
+ # Configure the broker (currently supports amqp:// URIs)
77
+ broker = "amqp://guest:guest@localhost:5672/"
76
78
  # Register the modules that the worker should load to find your routines
77
79
  register = ["basic"]
78
80
  ```
79
81
 
80
- The pika configuration can be overridden with an environment variable
82
+ The broker configuration can be overridden with an environment variable
81
83
  to allow a project to be deployed in multiple environments.
82
84
 
83
85
  ```sh
84
- QUEUEIO_PIKA='amqp://guest:guest@localhost:5672/'
86
+ QUEUEIO_BROKER='amqp://guest:guest@localhost:5672/'
85
87
  ```
86
88
 
87
89
  Sync the queues to the broker:
@@ -46,17 +46,17 @@ 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
60
  ```
61
61
 
62
62
  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.7.0"
4
4
  description = "Python background queues with an async twist"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -29,6 +29,7 @@ dependencies = [
29
29
 
30
30
  [project.optional-dependencies]
31
31
  django = ["django>=6.0", "django-cmd>=3.0"]
32
+ psycopg = ["psycopg>=3.3"]
32
33
 
33
34
  [project.entry-points.djp]
34
35
  queueio = "queueio.django.djp"
@@ -42,6 +43,7 @@ dev = [
42
43
  "django-extensions",
43
44
  "djp",
44
45
  "queueio[django]",
46
+ "queueio[psycopg]",
45
47
  "mkdocs-material>=9.7.1",
46
48
  "mkdocs-same-dir>=0.1.3",
47
49
  "pytest>=8.4.1",
@@ -50,6 +52,7 @@ dev = [
50
52
  "ruff>=0.9.6",
51
53
  "textual-dev>=1.7.0",
52
54
  "types-pika>=1.2.0b1",
55
+ "psycopg>=3.3.3",
53
56
  ]
54
57
 
55
58
  [tool.ruff.lint]
@@ -65,7 +68,7 @@ register = [
65
68
  "queueio.samples.priority",
66
69
  "queueio.samples.queuevar",
67
70
  ]
68
- pika = "amqp://localhost:5672"
71
+ broker = "amqp://localhost:5672"
69
72
 
70
73
  [tool.pytest.ini_options]
71
74
  timeout = 30
@@ -0,0 +1,5 @@
1
+ {
2
+ "pythonVersion": "3.14",
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,4 +1,3 @@
1
- from contextlib import suppress
2
1
  from typing import Annotated
3
2
 
4
3
  import typer
@@ -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."""
@@ -60,7 +60,7 @@ class BaseBrokerTest:
60
60
  @pytest.mark.timeout(2)
61
61
  def test_prefetch_limits_message_consumption(self, broker):
62
62
  """Verify that prefetch parameter limits message consumption."""
63
- broker.create(queue="test-queue")
63
+ broker.sync(["test-queue"])
64
64
  broker.purge(queue="test-queue")
65
65
 
66
66
  # Enqueue more messages than prefetch limit
@@ -93,7 +93,7 @@ class BaseBrokerTest:
93
93
  @pytest.mark.timeout(2)
94
94
  def test_suspend_resume_affects_prefetch_capacity(self, broker):
95
95
  """Verify suspending messages frees capacity and resuming reduces it."""
96
- broker.create(queue="test-queue")
96
+ broker.sync(["test-queue"])
97
97
  broker.purge(queue="test-queue")
98
98
 
99
99
  # Enqueue messages
@@ -131,7 +131,7 @@ class BaseBrokerTest:
131
131
  @pytest.mark.timeout(2)
132
132
  def test_complete_message_frees_prefetch_capacity(self, broker):
133
133
  """Verify completing messages frees up capacity."""
134
- broker.create(queue="test-queue")
134
+ broker.sync(["test-queue"])
135
135
  broker.purge(queue="test-queue")
136
136
 
137
137
  # Enqueue more messages than prefetch limit
@@ -171,7 +171,7 @@ class BaseBrokerTest:
171
171
  @pytest.mark.timeout(2)
172
172
  def test_multiple_receivers_independent_prefetch_limits(self, broker):
173
173
  """Verify multiple receivers operate with independent prefetch limits."""
174
- broker.create(queue="test-queue")
174
+ broker.sync(["test-queue"])
175
175
  broker.purge(queue="test-queue")
176
176
 
177
177
  # Enqueue enough messages for both receivers
@@ -223,8 +223,7 @@ class BaseBrokerTest:
223
223
  @skip_if_unsupported("supports_multiple_queues")
224
224
  def test_receive_supports_multiple_queues(self, broker):
225
225
  """Verify broker can receive from multiple queues."""
226
- broker.create(queue="queue1")
227
- broker.create(queue="queue2")
226
+ broker.sync(["queue1", "queue2"])
228
227
  broker.purge(queue="queue1")
229
228
  broker.purge(queue="queue2")
230
229
 
@@ -260,10 +259,7 @@ class BaseBrokerTest:
260
259
  def test_multiple_queues_with_mixed_empty_and_filled(self, broker):
261
260
  """Verify broker handles multiple queues with some empty, some with messages."""
262
261
 
263
- broker.create(queue="empty1")
264
- broker.create(queue="filled")
265
- broker.create(queue="empty2")
266
- broker.create(queue="also_filled")
262
+ broker.sync(["empty1", "filled", "empty2", "also_filled"])
267
263
  broker.purge(queue="empty1")
268
264
  broker.purge(queue="filled")
269
265
  broker.purge(queue="empty2")
@@ -308,8 +304,7 @@ class BaseBrokerTest:
308
304
  probability in the round-robin selection.
309
305
  """
310
306
 
311
- broker.create(queue="priority_queue")
312
- broker.create(queue="normal_queue")
307
+ broker.sync(["priority_queue", "normal_queue"])
313
308
  broker.purge(queue="priority_queue")
314
309
  broker.purge(queue="normal_queue")
315
310
 
@@ -410,9 +405,7 @@ class BaseBrokerTest:
410
405
  the message, not just cycle by a fixed amount.
411
406
  """
412
407
 
413
- broker.create(queue="empty")
414
- broker.create(queue="queue1")
415
- broker.create(queue="queue2")
408
+ broker.sync(["empty", "queue1", "queue2"])
416
409
  broker.purge(queue="empty")
417
410
  broker.purge(queue="queue1")
418
411
  broker.purge(queue="queue2")
@@ -473,7 +466,7 @@ class BaseBrokerTest:
473
466
  @pytest.mark.timeout(2)
474
467
  def test_higher_priority_consumed_first(self, broker):
475
468
  """Verify that higher priority messages are consumed before lower ones."""
476
- broker.create(queue="test-queue")
469
+ broker.sync(["test-queue"])
477
470
  broker.purge(queue="test-queue")
478
471
 
479
472
  # Enqueue low priority first, then high priority
@@ -22,7 +22,11 @@ def test_all_queueio_modules_preserve_freethreading():
22
22
  for _, modname, _ in pkgutil.walk_packages(
23
23
  queueio.__path__, f"{queueio.__name__}."
24
24
  ):
25
- if not modname.endswith("_test") and ".django" not in modname:
25
+ if (
26
+ not modname.endswith("_test")
27
+ and ".django" not in modname
28
+ and ".psycopg" not in modname
29
+ ):
26
30
  importlib.import_module(modname)
27
31
 
28
32
  assert sys._is_gil_enabled() is False
@@ -4,12 +4,6 @@ from collections.abc import Iterator
4
4
 
5
5
 
6
6
  class Journal(ABC):
7
- @classmethod
8
- @abstractmethod
9
- def from_uri(cls, uri: str, /):
10
- """Create a journal instance from a URI."""
11
- raise NotImplementedError("Subclasses must implement this method.")
12
-
13
7
  @abstractmethod
14
8
  def subscribe(self) -> Iterator[bytes]:
15
9
  raise NotImplementedError("Subclasses must implement this method.")
@@ -17,9 +17,9 @@ class Monitor(App):
17
17
 
18
18
  TITLE = "queueio Monitor"
19
19
 
20
- def __init__(self):
20
+ def __init__(self, queueio: QueueIO):
21
21
  super().__init__()
22
- self.__queueio = QueueIO()
22
+ self.__queueio = queueio
23
23
  self.__thread = Thread(target=self.__listen)
24
24
  self.__events = self.__queueio.subscribe(
25
25
  {
@@ -0,0 +1,31 @@
1
+ from collections.abc import Generator
2
+ from contextlib import AbstractContextManager
3
+ from contextlib import contextmanager
4
+
5
+ from pika import URLParameters
6
+
7
+ from queueio.backend import Backend
8
+
9
+ from .broker import PikaBroker
10
+ from .journal import PikaJournal
11
+ from .threadsafe import ThreadsafeConnection
12
+
13
+
14
+ class PikaBackend(Backend):
15
+ def __init__(self, connection: ThreadsafeConnection):
16
+ self.__connection = connection
17
+
18
+ @classmethod
19
+ @contextmanager
20
+ def connect(cls, uri: str, /) -> Generator[PikaBackend]:
21
+ connection = ThreadsafeConnection(URLParameters(uri))
22
+ try:
23
+ yield cls(connection)
24
+ finally:
25
+ connection.close()
26
+
27
+ def broker(self) -> AbstractContextManager[PikaBroker]:
28
+ return PikaBroker.connect(self.__connection)
29
+
30
+ def journal(self) -> AbstractContextManager[PikaJournal]:
31
+ return PikaJournal.connect(self.__connection)
@@ -1,7 +1,9 @@
1
+ from collections.abc import Generator
2
+ from collections.abc import Iterable
3
+ from contextlib import contextmanager
4
+ from contextlib import suppress
1
5
  from threading import Lock
2
6
 
3
- from pika import URLParameters
4
- from pika.connection import Parameters
5
7
  from pika.spec import BasicProperties
6
8
 
7
9
  from queueio.broker import Broker
@@ -15,17 +17,34 @@ class PikaBroker(Broker):
15
17
  """A broker enables producing and consuming messages on a queue."""
16
18
 
17
19
  @classmethod
18
- def from_uri(cls, uri: str, /):
19
- """Create a broker instance from a URI."""
20
- return cls(URLParameters(uri))
20
+ @contextmanager
21
+ def connect(cls, connection: ThreadsafeConnection) -> Generator[PikaBroker]:
22
+ broker = cls(connection)
23
+ try:
24
+ yield broker
25
+ finally:
26
+ broker.shutdown()
21
27
 
22
- def __init__(self, connection_params: Parameters):
23
- self.__connection = ThreadsafeConnection(connection_params)
28
+ def __init__(self, connection: ThreadsafeConnection):
29
+ self.__connection = connection
24
30
  self.__channel = self.__connection.channel()
25
31
  self.__shutdown_lock = Lock()
26
32
  self.__shutdown = False
27
33
  self.__receivers = set[PikaReceiver]()
28
34
 
35
+ def sync(self, queues: Iterable[str], *, recreate: bool = False):
36
+ channel = self.__connection.channel()
37
+ try:
38
+ for queue in queues:
39
+ if recreate:
40
+ with suppress(Exception):
41
+ channel.delete(queue=queue)
42
+ channel.queue_declare(
43
+ queue=queue, durable=True, arguments={"x-max-priority": 9}
44
+ )
45
+ finally:
46
+ channel.close()
47
+
29
48
  def enqueue(self, body: bytes, /, *, queue: str, priority: int):
30
49
  self.__channel.publish(
31
50
  exchange="",
@@ -34,18 +53,6 @@ class PikaBroker(Broker):
34
53
  properties=BasicProperties(priority=priority),
35
54
  )
36
55
 
37
- def create(self, *, queue: str):
38
- channel = self.__connection.channel()
39
- channel.queue_declare(
40
- queue=queue, durable=True, arguments={"x-max-priority": 9}
41
- )
42
- channel.close()
43
-
44
- def delete(self, *, queue: str):
45
- channel = self.__connection.channel()
46
- channel.delete(queue=queue)
47
- channel.close()
48
-
49
56
  def purge(self, *, queue: str):
50
57
  self.__channel.purge(queue=queue)
51
58
 
@@ -62,7 +69,4 @@ class PikaBroker(Broker):
62
69
  self.__shutdown = True
63
70
  for receiver in self.__receivers:
64
71
  receiver.shutdown()
65
- self.__connection.close()
66
-
67
- def __del__(self):
68
- self.shutdown()
72
+ self.__channel.close()
@@ -0,0 +1,18 @@
1
+ import pytest
2
+
3
+ from queueio.broker_test import BaseBrokerTest
4
+
5
+ from . import PikaBackend
6
+
7
+
8
+ class TestPikaBroker(BaseBrokerTest):
9
+ supports_multiple_queues = True
10
+ supports_weighted_queue_subscriptions = False
11
+
12
+ @pytest.fixture
13
+ def broker(self):
14
+ with (
15
+ PikaBackend.connect("amqp://localhost:5672") as backend,
16
+ backend.broker() as broker,
17
+ ):
18
+ yield broker
@@ -1,10 +1,9 @@
1
+ from collections.abc import Generator
1
2
  from collections.abc import Iterator
3
+ from contextlib import contextmanager
2
4
  from threading import Lock
3
5
  from typing import cast
4
6
 
5
- from pika import URLParameters
6
- from pika.connection import Parameters
7
-
8
7
  from queueio.journal import Journal
9
8
 
10
9
  from .threadsafe import ThreadsafeConnection
@@ -12,14 +11,17 @@ from .threadsafe import ThreadsafeConnection
12
11
 
13
12
  class PikaJournal(Journal):
14
13
  @classmethod
15
- def from_uri(cls, uri: str, /):
16
- """Create a journal instance from a URI."""
17
- return cls(URLParameters(uri))
18
-
19
- def __init__(self, connection_params: Parameters):
20
- self.__connection = ThreadsafeConnection(connection_params)
21
- self.__subscribe_channel = self.__connection.channel()
22
- self.__publish_channel = self.__connection.channel()
14
+ @contextmanager
15
+ def connect(cls, connection: ThreadsafeConnection) -> Generator[PikaJournal]:
16
+ journal = cls(connection)
17
+ try:
18
+ yield journal
19
+ finally:
20
+ journal.shutdown()
21
+
22
+ def __init__(self, connection: ThreadsafeConnection):
23
+ self.__subscribe_channel = connection.channel()
24
+ self.__publish_channel = connection.channel()
23
25
 
24
26
  declare_result = self.__subscribe_channel.queue_declare("", exclusive=True)
25
27
  self.__subscribe_queue = cast(str, declare_result.method.queue)
@@ -55,4 +57,3 @@ class PikaJournal(Journal):
55
57
  self.__subscribe_channel.cancel(self.__subscribe_consumer_tag)
56
58
  self.__subscribe_channel.close()
57
59
  self.__publish_channel.close()
58
- self.__connection.close()