taraqueue 0.0.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- taraqueue/__init__.py +0 -0
- taraqueue/queue.py +149 -0
- taraqueue/registry.py +76 -0
- taraqueue/testing/__init__.py +0 -0
- taraqueue/testing/compose.py +92 -0
- taraqueue/testing/queue.py +36 -0
- taraqueue/testing/services.py +87 -0
- taraqueue-0.0.1.dev0.dist-info/METADATA +67 -0
- taraqueue-0.0.1.dev0.dist-info/RECORD +12 -0
- taraqueue-0.0.1.dev0.dist-info/WHEEL +4 -0
- taraqueue-0.0.1.dev0.dist-info/entry_points.txt +7 -0
- taraqueue-0.0.1.dev0.dist-info/licenses/LICENSE.rst +21 -0
taraqueue/__init__.py
ADDED
|
File without changes
|
taraqueue/queue.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Queue abstraction and implementation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from contextlib import asynccontextmanager, suppress
|
|
7
|
+
from time import time
|
|
8
|
+
|
|
9
|
+
from attrs import define, field
|
|
10
|
+
from yarl import URL
|
|
11
|
+
|
|
12
|
+
from taraqueue.registry import registry_load
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QueueEmpty(Exception):
|
|
16
|
+
"""Raised when the queue is empty."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@define
|
|
20
|
+
class Queue(ABC):
|
|
21
|
+
"""Base queue class."""
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_url(cls, url: URL | str, registry=None) -> "Queue":
|
|
25
|
+
if registry is None:
|
|
26
|
+
registry = registry_load("taraqueue")
|
|
27
|
+
scheme = URL(url).scheme
|
|
28
|
+
queue_cls = registry["taraqueue"][scheme]
|
|
29
|
+
return queue_cls.from_url(url)
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
async def subscribe(self, topic: str) -> None:
|
|
33
|
+
"""Subscribe to a topic before receiving messages."""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
async def unsubscribe(self, topic: str) -> None:
|
|
37
|
+
"""Unsubscribe from a topic after receiving messages."""
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
async def receive(self, timeout=None) -> str:
|
|
41
|
+
"""Listen for messages on the subscribed topics."""
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
async def publish(self, topic: str, message: str) -> None:
|
|
45
|
+
"""Publish a message to a topic."""
|
|
46
|
+
|
|
47
|
+
@asynccontextmanager
|
|
48
|
+
async def connect(self, topic: str):
|
|
49
|
+
"""Context manager that subscribes on entry and unsubscribes on exit."""
|
|
50
|
+
await self.subscribe(topic)
|
|
51
|
+
try:
|
|
52
|
+
yield self
|
|
53
|
+
finally:
|
|
54
|
+
await self.unsubscribe(topic)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_global_memory_queues = defaultdict(list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@define
|
|
61
|
+
class MemoryQueue(Queue):
|
|
62
|
+
|
|
63
|
+
topics = field(factory=list)
|
|
64
|
+
queues = field(default=_global_memory_queues)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_url(cls, url: URL) -> "MemoryQueue":
|
|
68
|
+
return cls()
|
|
69
|
+
|
|
70
|
+
async def subscribe(self, topic: str) -> None:
|
|
71
|
+
"""See `Queue.subscribe`."""
|
|
72
|
+
self.topics.append(topic)
|
|
73
|
+
|
|
74
|
+
async def unsubscribe(self, topic: str) -> None:
|
|
75
|
+
"""See `Queue.unsubscribe`."""
|
|
76
|
+
with suppress(ValueError):
|
|
77
|
+
self.topics.remove(topic)
|
|
78
|
+
|
|
79
|
+
async def receive(self, timeout=None) -> str:
|
|
80
|
+
"""See `Queue.receive`."""
|
|
81
|
+
for topic in self.topics[:]:
|
|
82
|
+
# Cycle through topics.
|
|
83
|
+
self.topics.append(self.topics.pop(0))
|
|
84
|
+
queue = self.queues[topic]
|
|
85
|
+
with suppress(IndexError):
|
|
86
|
+
return queue.pop(0)
|
|
87
|
+
|
|
88
|
+
raise QueueEmpty("Queue is empty")
|
|
89
|
+
|
|
90
|
+
async def publish(self, topic: str, message: str) -> None:
|
|
91
|
+
"""See `Queue.publish`."""
|
|
92
|
+
queue = self.queues[topic]
|
|
93
|
+
queue.append(message)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@define
|
|
97
|
+
class RedisQueue(Queue):
|
|
98
|
+
|
|
99
|
+
client = field()
|
|
100
|
+
pubsub = field()
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_env(cls, env=os.environ) -> "RedisQueue":
|
|
104
|
+
host = env.get("REDIS_SLAVEOF_IP", "") or env.get("IPV4_NETWORK", "172.22.1") + ".249"
|
|
105
|
+
port = int(env.get("REDIS_SLAVEOF_PORT", "") or "6379")
|
|
106
|
+
password = env.get("REDISPASS")
|
|
107
|
+
return cls.from_host(host, port, password=password)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_host(cls, host: str, port: int = 6379, password: str | None = None) -> "RedisQueue":
|
|
111
|
+
from redis.asyncio import StrictRedis
|
|
112
|
+
|
|
113
|
+
client = StrictRedis(
|
|
114
|
+
host=host,
|
|
115
|
+
port=port,
|
|
116
|
+
decode_responses=True,
|
|
117
|
+
db=0,
|
|
118
|
+
password=password,
|
|
119
|
+
)
|
|
120
|
+
pubsub = client.pubsub(ignore_subscribe_messages=True)
|
|
121
|
+
return cls(client, pubsub)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_url(cls, url: URL | str) -> "RedisQueue":
|
|
125
|
+
url = URL(url)
|
|
126
|
+
return cls.from_host(url.host, url.port, password=url.password)
|
|
127
|
+
|
|
128
|
+
async def subscribe(self, topic: str) -> None:
|
|
129
|
+
"""See `Queue.subscribe`."""
|
|
130
|
+
await self.pubsub.subscribe(topic)
|
|
131
|
+
|
|
132
|
+
async def unsubscribe(self, topic: str) -> None:
|
|
133
|
+
"""See `Queue.unsubscribe`."""
|
|
134
|
+
await self.pubsub.unsubscribe(topic)
|
|
135
|
+
|
|
136
|
+
async def receive(self, timeout=0) -> str:
|
|
137
|
+
"""See `Queue.receive`."""
|
|
138
|
+
stop_time = time() + timeout
|
|
139
|
+
while True:
|
|
140
|
+
remaining_timeout = max(0.0, stop_time - time())
|
|
141
|
+
message = await self.pubsub.get_message(ignore_subscribe_messages=True, timeout=remaining_timeout)
|
|
142
|
+
if message:
|
|
143
|
+
return message["data"]
|
|
144
|
+
if time() >= stop_time:
|
|
145
|
+
raise QueueEmpty("Queue is empty")
|
|
146
|
+
|
|
147
|
+
async def publish(self, topic: str, message: str) -> None:
|
|
148
|
+
"""See `Queue.publish`."""
|
|
149
|
+
await self.client.publish(topic, message)
|
taraqueue/registry.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Entry points based registry management."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from importlib.metadata import entry_points
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_entry_points(group):
|
|
8
|
+
"""Get the list of pytest_unique entry points."""
|
|
9
|
+
try:
|
|
10
|
+
return entry_points().select(group=group)
|
|
11
|
+
except AttributeError:
|
|
12
|
+
# Backward compatibility with Python 3.9.
|
|
13
|
+
return entry_points().get(group, [])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def registry_load(group, registry=None):
|
|
17
|
+
"""Find all installed entry points."""
|
|
18
|
+
if registry is None:
|
|
19
|
+
registry = {}
|
|
20
|
+
|
|
21
|
+
for entry_point in get_entry_points(group):
|
|
22
|
+
entry = entry_point.load()
|
|
23
|
+
registry_add(group, entry_point.name, entry, registry)
|
|
24
|
+
|
|
25
|
+
return registry
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def registry_add(group, name, entry, registry=None):
|
|
29
|
+
"""Add an entry to a registry.
|
|
30
|
+
|
|
31
|
+
:param group: Group of the entry.
|
|
32
|
+
:param name: Name of the entry.
|
|
33
|
+
:param entry: Entry to add.
|
|
34
|
+
:param registry: Optional registry to update.
|
|
35
|
+
:return: A registry with the entry.
|
|
36
|
+
"""
|
|
37
|
+
if registry is None:
|
|
38
|
+
registry = {
|
|
39
|
+
group: {},
|
|
40
|
+
}
|
|
41
|
+
else:
|
|
42
|
+
registry.setdefault(group, {})
|
|
43
|
+
|
|
44
|
+
registry[group][name] = entry
|
|
45
|
+
return registry
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def registry_remove(group, name, registry=None):
|
|
49
|
+
"""Remove an entry from a registry.
|
|
50
|
+
|
|
51
|
+
If the entry doesn't exist, return silently.
|
|
52
|
+
|
|
53
|
+
:param group: Group of the entry.
|
|
54
|
+
:param name: Name of the entry.
|
|
55
|
+
:param registry: Optional registry to update.
|
|
56
|
+
"""
|
|
57
|
+
if registry is not None:
|
|
58
|
+
with contextlib.suppress(KeyError):
|
|
59
|
+
del registry[group][name]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def registry_get(group, name, registry=None):
|
|
63
|
+
"""Get an entry from a registry.
|
|
64
|
+
|
|
65
|
+
If the registry is not defined or the group is not in the registry,
|
|
66
|
+
the registry is loaded again.
|
|
67
|
+
|
|
68
|
+
:param group: Group of the entry.
|
|
69
|
+
:param name: Name of the entry.
|
|
70
|
+
:param registry: Optional registry to get from.
|
|
71
|
+
:raises KeyError: If not found.
|
|
72
|
+
"""
|
|
73
|
+
if registry is None or group not in registry:
|
|
74
|
+
registry = registry_load(group, registry)
|
|
75
|
+
|
|
76
|
+
return registry[group][name]
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Compose server module."""
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from attrs import define
|
|
7
|
+
from more_itertools import only
|
|
8
|
+
from pytest_xdocker.docker import DockerContainer
|
|
9
|
+
from pytest_xdocker.process import ProcessData, ProcessServer
|
|
10
|
+
from pytest_xdocker.xdocker import xdocker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@define(frozen=True)
|
|
14
|
+
class ComposeService:
|
|
15
|
+
"""Compose service.
|
|
16
|
+
|
|
17
|
+
:param name: Name of the compose service container.
|
|
18
|
+
:param network: Network name to use when resolving the IP address.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
network: str | None = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def container(self):
|
|
26
|
+
return DockerContainer(self.name)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def container_id(self):
|
|
30
|
+
return self.container.inspect["Id"]
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def env(self):
|
|
34
|
+
env = self.container.inspect["Config"]["Env"]
|
|
35
|
+
return dict(e.split("=", 1) for e in env)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def ip(self):
|
|
39
|
+
network_settings = self.container.inspect["NetworkSettings"]
|
|
40
|
+
networks = network_settings["Networks"]
|
|
41
|
+
if self.network and self.network in networks:
|
|
42
|
+
return networks[self.network]["IPAddress"]
|
|
43
|
+
return only(networks.values())["IPAddress"]
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def started_at(self):
|
|
47
|
+
started_at = self.container.inspect["State"]["StartedAt"]
|
|
48
|
+
return datetime.fromisoformat(started_at)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ComposeServer(ProcessServer):
|
|
52
|
+
def __init__(self, pattern, project="test", env_file=None, compose_files=None, timeout=180, **kwargs):
|
|
53
|
+
"""Initilize a compose server."""
|
|
54
|
+
super().__init__(**kwargs)
|
|
55
|
+
self.pattern = pattern
|
|
56
|
+
self.project = project
|
|
57
|
+
self.env_file = env_file
|
|
58
|
+
self.compose_files = compose_files
|
|
59
|
+
self.timeout = timeout
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
return f"{self.__class__.__name__}(pattern={self.pattern!r}, project={self.project!r})"
|
|
63
|
+
|
|
64
|
+
def full_name(self, name):
|
|
65
|
+
return f"{self.project}-{name}-1"
|
|
66
|
+
|
|
67
|
+
def prepare_func(self, controldir):
|
|
68
|
+
"""Prepare the function to run the compose service."""
|
|
69
|
+
full_name = self.full_name(controldir.basename)
|
|
70
|
+
compose = xdocker.compose().with_project_name(self.project)
|
|
71
|
+
if env_file := self.env_file:
|
|
72
|
+
compose = compose.with_env_file(env_file)
|
|
73
|
+
for file in self.compose_files or []:
|
|
74
|
+
compose = compose.with_file(file)
|
|
75
|
+
|
|
76
|
+
command = (
|
|
77
|
+
compose
|
|
78
|
+
.run(controldir.basename)
|
|
79
|
+
.with_name(full_name)
|
|
80
|
+
.with_build()
|
|
81
|
+
.with_remove()
|
|
82
|
+
.with_optionals("--use-aliases")
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return ProcessData(self.pattern, command, timeout=self.timeout)
|
|
86
|
+
|
|
87
|
+
@contextmanager
|
|
88
|
+
def run(self, name):
|
|
89
|
+
"""Return an `ComposeService` to the running service."""
|
|
90
|
+
with super().run(name):
|
|
91
|
+
full_name = self.full_name(name)
|
|
92
|
+
yield ComposeService(full_name, network=f"{self.project}_default")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Queue fixtures."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from yarl import URL
|
|
5
|
+
|
|
6
|
+
from taraqueue.queue import Queue
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def memory_queue():
|
|
11
|
+
"""Memory queue fixture."""
|
|
12
|
+
url = URL.build(scheme="memory")
|
|
13
|
+
return Queue.from_url(url)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def redis_queue(redis_service, env_vars):
|
|
18
|
+
"""Redis queue fixture."""
|
|
19
|
+
url = URL.build(
|
|
20
|
+
scheme="redis",
|
|
21
|
+
host=redis_service.ip,
|
|
22
|
+
port=6379,
|
|
23
|
+
password=env_vars["REDISPASS"],
|
|
24
|
+
)
|
|
25
|
+
return Queue.from_url(url)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture(
|
|
29
|
+
params=[
|
|
30
|
+
"memory_queue",
|
|
31
|
+
"redis_queue",
|
|
32
|
+
],
|
|
33
|
+
)
|
|
34
|
+
def queue(request):
|
|
35
|
+
"""Queue fixture."""
|
|
36
|
+
return request.getfixturevalue(request.param)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Service fixtures."""
|
|
2
|
+
|
|
3
|
+
from functools import partial
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from taraqueue.testing.compose import ComposeServer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(scope="session")
|
|
12
|
+
def project():
|
|
13
|
+
return "test"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(scope="session")
|
|
17
|
+
def env_vars(project):
|
|
18
|
+
"""Environment variables for the services."""
|
|
19
|
+
return {
|
|
20
|
+
"COMPOSE_PROJECT_NAME": project,
|
|
21
|
+
"REDISPASS": "test",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture(scope="session")
|
|
26
|
+
def env_file(env_vars, request):
|
|
27
|
+
"""Environment file containing `env_vars`.
|
|
28
|
+
|
|
29
|
+
Cached for troubleshooting purposes.
|
|
30
|
+
"""
|
|
31
|
+
env_file = request.config.cache.makedir("compose") / "env"
|
|
32
|
+
with env_file.open("w") as f:
|
|
33
|
+
for k, v in env_vars.items():
|
|
34
|
+
f.write(f"{k}={v}\n")
|
|
35
|
+
|
|
36
|
+
return env_file
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture(scope="session")
|
|
40
|
+
def compose_files(request):
|
|
41
|
+
directory = Path(request.config.rootdir)
|
|
42
|
+
filenames = ["docker-compose.yml", "compose.yaml", "compose.yml"]
|
|
43
|
+
while True:
|
|
44
|
+
for filename in filenames:
|
|
45
|
+
path = directory / filename
|
|
46
|
+
if path.exists():
|
|
47
|
+
all_files = directory.glob(f"{path.stem}.*")
|
|
48
|
+
ordered_files = sorted(all_files, key=lambda p: len(p.name))
|
|
49
|
+
return list(ordered_files)
|
|
50
|
+
|
|
51
|
+
if directory == directory.parent:
|
|
52
|
+
raise FileNotFoundError("Docker compose file not found")
|
|
53
|
+
|
|
54
|
+
directory = directory.parent
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@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,
|
|
64
|
+
process=process,
|
|
65
|
+
)
|
|
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
|
+
with server.run("redis") as service:
|
|
73
|
+
yield service
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.fixture(scope="session")
|
|
77
|
+
def redis_client(redis_service, env_vars):
|
|
78
|
+
"""Redis client to the service fixture."""
|
|
79
|
+
from redis import StrictRedis
|
|
80
|
+
|
|
81
|
+
return StrictRedis(
|
|
82
|
+
host=redis_service.ip,
|
|
83
|
+
port=6379,
|
|
84
|
+
decode_responses=True,
|
|
85
|
+
db=0,
|
|
86
|
+
password=env_vars["REDISPASS"],
|
|
87
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: taraqueue
|
|
3
|
+
Version: 0.0.1.dev0
|
|
4
|
+
Summary: Python queue abstraction layer
|
|
5
|
+
Project-URL: Repository, https://github.com/taradix/taraqueue
|
|
6
|
+
Author-email: Marc Tardif <marc@taram.ca>
|
|
7
|
+
License-File: LICENSE.rst
|
|
8
|
+
Requires-Python: <4.0,>=3.12
|
|
9
|
+
Requires-Dist: attrs>=26.1.0
|
|
10
|
+
Requires-Dist: yarl>=1.23.0
|
|
11
|
+
Provides-Extra: check
|
|
12
|
+
Requires-Dist: ruff<1.0.0,>=0.15.0; extra == 'check'
|
|
13
|
+
Provides-Extra: docs
|
|
14
|
+
Requires-Dist: sphinx-rtd-theme<3.2.0,>=3.1.0; extra == 'docs'
|
|
15
|
+
Requires-Dist: sphinx<10.0.0,>=9.0.0; extra == 'docs'
|
|
16
|
+
Requires-Dist: sphinxcontrib-log-cabinet<2.0.0,>=1.0.1; extra == 'docs'
|
|
17
|
+
Provides-Extra: redis
|
|
18
|
+
Requires-Dist: redis>=7.4.0; extra == 'redis'
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: coverage<8.0.0,>=7.2.3; extra == 'test'
|
|
21
|
+
Requires-Dist: more-itertools<11.1.0,>=11.0.1; extra == 'test'
|
|
22
|
+
Requires-Dist: pytest-asyncio<2.0.0,>=1.0.0; extra == 'test'
|
|
23
|
+
Requires-Dist: pytest-unique<1.0.0,>=0.1.8; extra == 'test'
|
|
24
|
+
Requires-Dist: pytest-xdocker<1.0.0,>=0.2.8; extra == 'test'
|
|
25
|
+
Requires-Dist: pytest<10.0.0,>=9.0.0; extra == 'test'
|
|
26
|
+
Requires-Dist: redis>=7.4.0; extra == 'test'
|
|
27
|
+
Description-Content-Type: text/x-rst
|
|
28
|
+
|
|
29
|
+
TaraQueue
|
|
30
|
+
=========
|
|
31
|
+
|
|
32
|
+
Python queue abstraction layer.
|
|
33
|
+
|
|
34
|
+
.. image:: https://img.shields.io/badge/license-MIT-blue.svg
|
|
35
|
+
:target: https://github.com/taradix/taraqueue/blob/master/LICENSE
|
|
36
|
+
:alt: License
|
|
37
|
+
.. image:: https://img.shields.io/pypi/v/taraqueue.svg
|
|
38
|
+
:target: https://pypi.python.org/pypi/taraqueue/
|
|
39
|
+
:alt: PyPI
|
|
40
|
+
.. image:: https://img.shields.io/github/issues-raw/taradix/taraqueue.svg
|
|
41
|
+
:target: https://github.com/taradix/taraqueue/issues
|
|
42
|
+
:alt: Issues
|
|
43
|
+
|
|
44
|
+
Requirements
|
|
45
|
+
------------
|
|
46
|
+
|
|
47
|
+
You will need the following prerequisites to use taraqueue:
|
|
48
|
+
|
|
49
|
+
- Python 3.12, 3.13, 3.14
|
|
50
|
+
|
|
51
|
+
Installation
|
|
52
|
+
------------
|
|
53
|
+
|
|
54
|
+
To install taraqueue:
|
|
55
|
+
|
|
56
|
+
.. code-block:: bash
|
|
57
|
+
|
|
58
|
+
$ pip install taraqueue
|
|
59
|
+
|
|
60
|
+
Resources
|
|
61
|
+
---------
|
|
62
|
+
|
|
63
|
+
- `Documentation <https://taradix.github.io/taraqueue/>`_
|
|
64
|
+
- `Release Notes <http://github.com/taradix/taraqueue/blob/master/CHANGES.rst>`_
|
|
65
|
+
- `Issue Tracker <http://github.com/taradix/taraqueue/issues>`_
|
|
66
|
+
- `Source Code <http://github.com/taradix/taraqueue/>`_
|
|
67
|
+
- `PyPi <https://pypi.org/project/taraqueue/>`_
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
taraqueue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
taraqueue/queue.py,sha256=-ya3DVSLTjNQ1nmL1ltpJgPmTkdmQe2W2UHvhUGKfzI,4490
|
|
3
|
+
taraqueue/registry.py,sha256=YcF9FE21HWfzoFbGGtC4GBozj-g5YbQuWdOor9XoPrU,2036
|
|
4
|
+
taraqueue/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
taraqueue/testing/compose.py,sha256=wyI7dkhdXgQra0RcrYqye7RnYSv4WRSOMUVJRloKfDk,2896
|
|
6
|
+
taraqueue/testing/queue.py,sha256=ryIcwE7Txeybairo6HkW9bO9mH7A6Qj9RU3YDiUpG8M,677
|
|
7
|
+
taraqueue/testing/services.py,sha256=YmbsbAiX_kBVziwnNwkuvxn-82zhNE926oxF6V0SfzU,2213
|
|
8
|
+
taraqueue-0.0.1.dev0.dist-info/METADATA,sha256=Bd4BaAxkjIJJ5aJFwOK8gZdAfMjX4JbEw2OUAy-Llqk,2136
|
|
9
|
+
taraqueue-0.0.1.dev0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
+
taraqueue-0.0.1.dev0.dist-info/entry_points.txt,sha256=IDp7ErMVJHagqZeDuawQyIxa6ZmLF9Kh3Wv3is1up6c,186
|
|
11
|
+
taraqueue-0.0.1.dev0.dist-info/licenses/LICENSE.rst,sha256=aBUUonOC9jBjhQb0VIObBylPnlIVph0P0QnlnZ8z4BM,1068
|
|
12
|
+
taraqueue-0.0.1.dev0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Marc Tardif
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|