taraqueue 0.3.0__tar.gz → 0.5.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 (45) hide show
  1. {taraqueue-0.3.0 → taraqueue-0.5.0}/CHANGES.rst +14 -0
  2. {taraqueue-0.3.0 → taraqueue-0.5.0}/PKG-INFO +1 -1
  3. {taraqueue-0.3.0 → taraqueue-0.5.0}/compose.yml +1 -1
  4. taraqueue-0.5.0/taraqueue/memory.py +58 -0
  5. {taraqueue-0.3.0 → taraqueue-0.5.0}/taraqueue/redis.py +10 -5
  6. {taraqueue-0.3.0 → taraqueue-0.5.0}/taraqueue/testing/queue.py +2 -2
  7. {taraqueue-0.3.0 → taraqueue-0.5.0}/taraqueue/testing/services.py +15 -26
  8. taraqueue-0.5.0/tests/test_queue.py +75 -0
  9. taraqueue-0.5.0/tests/test_redis.py +6 -0
  10. {taraqueue-0.3.0 → taraqueue-0.5.0}/uv.lock +172 -162
  11. taraqueue-0.3.0/taraqueue/memory.py +0 -50
  12. taraqueue-0.3.0/tests/test_queue.py +0 -22
  13. {taraqueue-0.3.0 → taraqueue-0.5.0}/.editorconfig +0 -0
  14. {taraqueue-0.3.0 → taraqueue-0.5.0}/.gitattributes +0 -0
  15. {taraqueue-0.3.0 → taraqueue-0.5.0}/.github/CODEOWNERS +0 -0
  16. {taraqueue-0.3.0 → taraqueue-0.5.0}/.github/actions/setup-uv-env/action.yml +0 -0
  17. {taraqueue-0.3.0 → taraqueue-0.5.0}/.github/renovate.json +0 -0
  18. {taraqueue-0.3.0 → taraqueue-0.5.0}/.github/workflows/publish.yml +0 -0
  19. {taraqueue-0.3.0 → taraqueue-0.5.0}/.github/workflows/renovate.yaml +0 -0
  20. {taraqueue-0.3.0 → taraqueue-0.5.0}/.github/workflows/test.yml +0 -0
  21. {taraqueue-0.3.0 → taraqueue-0.5.0}/.gitignore +0 -0
  22. {taraqueue-0.3.0 → taraqueue-0.5.0}/.vscode/extensions.json +0 -0
  23. {taraqueue-0.3.0 → taraqueue-0.5.0}/.vscode/launch.json +0 -0
  24. {taraqueue-0.3.0 → taraqueue-0.5.0}/.vscode/settings.json +0 -0
  25. {taraqueue-0.3.0 → taraqueue-0.5.0}/CONTRIBUTING.rst +0 -0
  26. {taraqueue-0.3.0 → taraqueue-0.5.0}/LICENSE.rst +0 -0
  27. {taraqueue-0.3.0 → taraqueue-0.5.0}/Makefile +0 -0
  28. {taraqueue-0.3.0 → taraqueue-0.5.0}/README.rst +0 -0
  29. {taraqueue-0.3.0 → taraqueue-0.5.0}/STYLE_GUIDE.rst +0 -0
  30. {taraqueue-0.3.0 → taraqueue-0.5.0}/docs/changes.rst +0 -0
  31. {taraqueue-0.3.0 → taraqueue-0.5.0}/docs/conf.py +0 -0
  32. {taraqueue-0.3.0 → taraqueue-0.5.0}/docs/contributing.rst +0 -0
  33. {taraqueue-0.3.0 → taraqueue-0.5.0}/docs/index.rst +0 -0
  34. {taraqueue-0.3.0 → taraqueue-0.5.0}/docs/license.rst +0 -0
  35. {taraqueue-0.3.0 → taraqueue-0.5.0}/docs/modules.rst +0 -0
  36. {taraqueue-0.3.0 → taraqueue-0.5.0}/docs/style_guide.rst +0 -0
  37. {taraqueue-0.3.0 → taraqueue-0.5.0}/docs/taraqueue.rst +0 -0
  38. {taraqueue-0.3.0 → taraqueue-0.5.0}/docs/taraqueue.testing.rst +0 -0
  39. {taraqueue-0.3.0 → taraqueue-0.5.0}/pyproject.toml +0 -0
  40. {taraqueue-0.3.0 → taraqueue-0.5.0}/taraqueue/__init__.py +0 -0
  41. {taraqueue-0.3.0 → taraqueue-0.5.0}/taraqueue/registry.py +0 -0
  42. {taraqueue-0.3.0 → taraqueue-0.5.0}/taraqueue/testing/__init__.py +0 -0
  43. {taraqueue-0.3.0 → taraqueue-0.5.0}/taraqueue/testing/compose.py +0 -0
  44. {taraqueue-0.3.0 → taraqueue-0.5.0}/tests/test_compose.py +0 -0
  45. {taraqueue-0.3.0 → taraqueue-0.5.0}/tests/test_registry.py +0 -0
@@ -1,3 +1,17 @@
1
+ Version 0.5.0
2
+ -------------
3
+
4
+ Released 2026-05-13
5
+
6
+ - Fix MemoryQueue to behave like RedisQueue.
7
+
8
+ Version 0.4.0
9
+ -------------
10
+
11
+ Released 2026-05-07
12
+
13
+ - Namespace generic pytest fixtures.
14
+
1
15
  Version 0.3.0
2
16
  -------------
3
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taraqueue
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Python queue abstraction layer
5
5
  Project-URL: Repository, https://github.com/taradix/taraqueue
6
6
  Author-email: Marc Tardif <marc@taram.ca>
@@ -1,4 +1,4 @@
1
1
  services:
2
2
  redis:
3
- image: redis:8.6.2-alpine
3
+ image: redis:8.6.3-alpine
4
4
  command: redis-server --requirepass ${REDIS_PASSWORD:-test}
@@ -0,0 +1,58 @@
1
+ """Memory queue implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from contextlib import suppress
7
+ from dataclasses import dataclass, field
8
+
9
+ from yarl import URL
10
+
11
+ from taraqueue import Queue, QueueEmpty
12
+
13
+ _channels: dict[str, list[asyncio.Queue[str]]] = {}
14
+
15
+
16
+ @dataclass
17
+ class MemoryQueue(Queue):
18
+ """In-process queue with fan-out semantics."""
19
+
20
+ queue: asyncio.Queue[str] = field(default_factory=asyncio.Queue)
21
+
22
+ @classmethod
23
+ def from_url(cls, url: URL) -> MemoryQueue:
24
+ return cls()
25
+
26
+ async def subscribe(self, topic: str) -> None:
27
+ """See `Queue.subscribe`."""
28
+ subscribers = _channels.setdefault(topic, [])
29
+ if self.queue in subscribers:
30
+ return
31
+ subscribers.append(self.queue)
32
+
33
+ async def unsubscribe(self, topic: str) -> None:
34
+ """See `Queue.unsubscribe`."""
35
+ subscribers = _channels.get(topic)
36
+ if subscribers is None:
37
+ return
38
+ with suppress(ValueError):
39
+ subscribers.remove(self.queue)
40
+ if not subscribers:
41
+ _channels.pop(topic, None)
42
+
43
+ async def receive(self, timeout=None) -> str:
44
+ """See `Queue.receive`."""
45
+ if not timeout:
46
+ try:
47
+ return self.queue.get_nowait()
48
+ except asyncio.QueueEmpty as e:
49
+ raise QueueEmpty("Queue is empty") from e
50
+ try:
51
+ return await asyncio.wait_for(self.queue.get(), timeout=float(timeout))
52
+ except TimeoutError as e:
53
+ raise QueueEmpty("Timed out waiting for message") from e
54
+
55
+ async def publish(self, topic: str, message: str) -> None:
56
+ """See `Queue.publish`."""
57
+ for q in list(_channels.get(topic, [])):
58
+ await q.put(message)
@@ -1,15 +1,20 @@
1
1
  """Redis queue implementation."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import os
4
6
  from dataclasses import dataclass, field
5
7
  from time import time
8
+ from typing import TYPE_CHECKING
6
9
 
7
- from redis.asyncio import Redis
8
- from redis.asyncio.client import PubSub
9
10
  from yarl import URL
10
11
 
11
12
  from taraqueue import Queue, QueueEmpty
12
13
 
14
+ if TYPE_CHECKING:
15
+ from redis.asyncio import Redis
16
+ from redis.asyncio.client import PubSub
17
+
13
18
 
14
19
  @dataclass
15
20
  class RedisQueue(Queue):
@@ -18,14 +23,14 @@ class RedisQueue(Queue):
18
23
  pubsub: PubSub = field()
19
24
 
20
25
  @classmethod
21
- def from_env(cls, env=os.environ) -> "RedisQueue":
26
+ def from_env(cls, env=os.environ) -> RedisQueue:
22
27
  host = env.get("REDIS_SLAVEOF_IP", "") or env.get("IPV4_NETWORK", "172.22.1") + ".249"
23
28
  port = int(env.get("REDIS_SLAVEOF_PORT", "") or "6379")
24
29
  password = env.get("REDIS_PASSWORD")
25
30
  return cls.from_host(host, port, password=password)
26
31
 
27
32
  @classmethod
28
- def from_host(cls, host: str, port: int = 6379, password: str | None = None) -> "RedisQueue":
33
+ def from_host(cls, host: str, port: int = 6379, password: str | None = None) -> RedisQueue:
29
34
  from redis.asyncio import StrictRedis
30
35
 
31
36
  client = StrictRedis(
@@ -39,7 +44,7 @@ class RedisQueue(Queue):
39
44
  return cls(client, pubsub)
40
45
 
41
46
  @classmethod
42
- def from_url(cls, url: URL | str) -> "RedisQueue":
47
+ def from_url(cls, url: URL | str) -> RedisQueue:
43
48
  url = URL(url)
44
49
  return cls.from_host(url.host, url.port, password=url.password)
45
50
 
@@ -14,13 +14,13 @@ def memory_queue():
14
14
 
15
15
 
16
16
  @pytest.fixture
17
- def redis_queue(redis_service, env_vars):
17
+ def redis_queue(redis_service, taraqueue_env_vars):
18
18
  """Redis queue fixture."""
19
19
  url = URL.build(
20
20
  scheme="redis",
21
21
  host=redis_service.ip,
22
22
  port=6379,
23
- password=env_vars["REDIS_PASSWORD"],
23
+ password=taraqueue_env_vars["REDIS_PASSWORD"],
24
24
  )
25
25
  return Queue.from_url(url)
26
26
 
@@ -1,6 +1,5 @@
1
1
  """Service fixtures."""
2
2
 
3
- from functools import partial
4
3
  from pathlib import Path
5
4
 
6
5
  import pytest
@@ -9,35 +8,31 @@ from taraqueue.testing.compose import ComposeServer
9
8
 
10
9
 
11
10
  @pytest.fixture(scope="session")
12
- def project():
13
- return "test"
14
-
15
-
16
- @pytest.fixture(scope="session")
17
- def env_vars(project):
11
+ def taraqueue_env_vars():
18
12
  """Environment variables for the services."""
19
13
  return {
20
- "COMPOSE_PROJECT_NAME": project,
14
+ "COMPOSE_PROJECT_NAME": "test",
21
15
  "REDIS_PASSWORD": "test",
22
16
  }
23
17
 
24
18
 
25
19
  @pytest.fixture(scope="session")
26
- def env_file(env_vars, request):
27
- """Environment file containing `env_vars`.
20
+ def taraqueue_env_file(taraqueue_env_vars, request):
21
+ """Environment file containing `taraqueue_env_vars`.
28
22
 
29
23
  Cached for troubleshooting purposes.
30
24
  """
31
25
  env_file = request.config.cache.makedir("compose") / "env"
32
26
  with env_file.open("w") as f:
33
- for k, v in env_vars.items():
27
+ for k, v in taraqueue_env_vars.items():
34
28
  f.write(f"{k}={v}\n")
35
29
 
36
30
  return env_file
37
31
 
38
32
 
39
33
  @pytest.fixture(scope="session")
40
- def compose_files(request):
34
+ def taraqueue_compose_files(request):
35
+ """Use the compose files from the project - not this library."""
41
36
  directory = Path(request.config.rootdir)
42
37
  filenames = ["docker-compose.yml", "compose.yaml", "compose.yml"]
43
38
  while True:
@@ -55,26 +50,20 @@ def compose_files(request):
55
50
 
56
51
 
57
52
  @pytest.fixture(scope="session")
58
- def compose_server(project, env_file, compose_files, process):
59
- return partial(
60
- ComposeServer,
61
- project=project,
62
- env_file=env_file,
63
- compose_files=compose_files,
53
+ def redis_service(process, taraqueue_env_file, taraqueue_compose_files):
54
+ """Redis service fixture."""
55
+ server = ComposeServer(
56
+ pattern="Ready to accept connections tcp",
57
+ env_file=taraqueue_env_file,
58
+ compose_files=taraqueue_compose_files,
64
59
  process=process,
65
60
  )
66
-
67
-
68
- @pytest.fixture(scope="session")
69
- def redis_service(compose_server):
70
- """Redis service fixture."""
71
- server = compose_server("Ready to accept connections tcp")
72
61
  with server.run("redis") as service:
73
62
  yield service
74
63
 
75
64
 
76
65
  @pytest.fixture(scope="session")
77
- def redis_client(redis_service, env_vars):
66
+ def redis_client(redis_service, taraqueue_env_vars):
78
67
  """Redis client to the service fixture."""
79
68
  from redis import StrictRedis
80
69
 
@@ -83,5 +72,5 @@ def redis_client(redis_service, env_vars):
83
72
  port=6379,
84
73
  decode_responses=True,
85
74
  db=0,
86
- password=env_vars["REDIS_PASSWORD"],
75
+ password=taraqueue_env_vars["REDIS_PASSWORD"],
87
76
  )
@@ -0,0 +1,75 @@
1
+ """Integration tests for the queue module."""
2
+
3
+
4
+ import pytest
5
+
6
+ from taraqueue import Queue, QueueEmpty
7
+
8
+
9
+ async def test_queue_send_receive(queue, unique):
10
+ """Sending a message to a queue should be the next received message."""
11
+ topic = unique("text")
12
+ async with queue.connect(topic) as session:
13
+ await session.publish(topic, "test")
14
+ result = await session.receive(5)
15
+ assert result == "test"
16
+
17
+
18
+ async def test_queue_receive_empty(queue, unique):
19
+ """Receiving from an empty queue should raise a QueueEmpty error."""
20
+ topic = unique("text")
21
+ async with queue.connect(topic) as session:
22
+ with pytest.raises(QueueEmpty):
23
+ await session.receive()
24
+
25
+
26
+ async def test_queue_two_subscribers_both_receive(unique):
27
+ """Every active subscriber should independently receive each published message.
28
+
29
+ This verifies fan-out semantics: two separate Queue instances subscribed to
30
+ the same channel both receive the message, rather than one consuming it and
31
+ the other seeing nothing.
32
+ """
33
+ topic = unique("text")
34
+ sub1 = Queue.from_url("memory://")
35
+ sub2 = Queue.from_url("memory://")
36
+ publisher = Queue.from_url("memory://")
37
+
38
+ async with sub1.connect(topic), sub2.connect(topic):
39
+ await publisher.publish(topic, "hello")
40
+ msg1 = await sub1.receive(timeout=5)
41
+ msg2 = await sub2.receive(timeout=5)
42
+
43
+ assert msg1 == "hello"
44
+ assert msg2 == "hello"
45
+
46
+
47
+ async def test_queue_subscribe_two_topics_receives_from_both(unique):
48
+ """A single subscriber connected to two topics should receive from either."""
49
+ topic_a = unique("text")
50
+ topic_b = unique("text")
51
+ sub = Queue.from_url("memory://")
52
+ publisher = Queue.from_url("memory://")
53
+
54
+ async with sub.connect(topic_a), sub.connect(topic_b):
55
+ await publisher.publish(topic_a, "from-a")
56
+ await publisher.publish(topic_b, "from-b")
57
+ msg1 = await sub.receive(timeout=5)
58
+ msg2 = await sub.receive(timeout=5)
59
+
60
+ assert {msg1, msg2} == {"from-a", "from-b"}
61
+
62
+ """A subscriber that connects after a publish should not receive that message.
63
+
64
+ This matches Redis pub/sub behaviour: messages are only delivered to
65
+ subscribers that are active at the moment of publishing.
66
+ """
67
+ topic = unique("text")
68
+ publisher = Queue.from_url("memory://")
69
+ late_sub = Queue.from_url("memory://")
70
+
71
+ await publisher.publish(topic, "early")
72
+
73
+ async with late_sub.connect(topic):
74
+ with pytest.raises(QueueEmpty):
75
+ await late_sub.receive()
@@ -0,0 +1,6 @@
1
+ """Integration tests for the Redis service."""
2
+
3
+
4
+ def test_redis_service(redis_client):
5
+ """The Redis service should return true on PING."""
6
+ assert redis_client.ping()