kuu 0.0.1b1__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 (41) hide show
  1. kuu-0.0.1b1/PKG-INFO +56 -0
  2. kuu-0.0.1b1/kuu/__init__.py +29 -0
  3. kuu-0.0.1b1/kuu/_import.py +140 -0
  4. kuu-0.0.1b1/kuu/_types.py +9 -0
  5. kuu-0.0.1b1/kuu/app.py +172 -0
  6. kuu-0.0.1b1/kuu/brokers/__init__.py +3 -0
  7. kuu-0.0.1b1/kuu/brokers/base.py +32 -0
  8. kuu-0.0.1b1/kuu/brokers/memory.py +142 -0
  9. kuu-0.0.1b1/kuu/brokers/nats.py +172 -0
  10. kuu-0.0.1b1/kuu/brokers/redis.py +207 -0
  11. kuu-0.0.1b1/kuu/cli.py +117 -0
  12. kuu-0.0.1b1/kuu/context.py +21 -0
  13. kuu-0.0.1b1/kuu/events.py +60 -0
  14. kuu-0.0.1b1/kuu/exceptions.py +23 -0
  15. kuu-0.0.1b1/kuu/handle.py +52 -0
  16. kuu-0.0.1b1/kuu/message.py +34 -0
  17. kuu-0.0.1b1/kuu/middleware/__init__.py +14 -0
  18. kuu-0.0.1b1/kuu/middleware/base.py +29 -0
  19. kuu-0.0.1b1/kuu/middleware/logging.py +37 -0
  20. kuu-0.0.1b1/kuu/middleware/results.py +53 -0
  21. kuu-0.0.1b1/kuu/middleware/retry.py +36 -0
  22. kuu-0.0.1b1/kuu/middleware/timeout.py +20 -0
  23. kuu-0.0.1b1/kuu/outcome.py +24 -0
  24. kuu-0.0.1b1/kuu/prometheus.py +179 -0
  25. kuu-0.0.1b1/kuu/registry.py +25 -0
  26. kuu-0.0.1b1/kuu/result.py +12 -0
  27. kuu-0.0.1b1/kuu/results/__init__.py +3 -0
  28. kuu-0.0.1b1/kuu/results/base.py +44 -0
  29. kuu-0.0.1b1/kuu/results/redis.py +57 -0
  30. kuu-0.0.1b1/kuu/scheduler/__init__.py +8 -0
  31. kuu-0.0.1b1/kuu/scheduler/job.py +54 -0
  32. kuu-0.0.1b1/kuu/scheduler/scheduler.py +259 -0
  33. kuu-0.0.1b1/kuu/serializers/__init__.py +11 -0
  34. kuu-0.0.1b1/kuu/serializers/base.py +21 -0
  35. kuu-0.0.1b1/kuu/serializers/json.py +53 -0
  36. kuu-0.0.1b1/kuu/serializers/msgpack.py +28 -0
  37. kuu-0.0.1b1/kuu/serializers/pickle.py +39 -0
  38. kuu-0.0.1b1/kuu/task.py +71 -0
  39. kuu-0.0.1b1/kuu/worker.py +165 -0
  40. kuu-0.0.1b1/pyproject.toml +82 -0
  41. kuu-0.0.1b1/readme.md +14 -0
kuu-0.0.1b1/PKG-INFO ADDED
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: kuu
3
+ Version: 0.0.1b1
4
+ Summary: lightweight async distributed task queue
5
+ Keywords: apscheduler,message queue,queue,schedule tasks,scheduler,scheduling,task,task queue
6
+ Author: Alexey Pechenin
7
+ Author-email: Alexey Pechenin <me@pyrorhythm.dev>
8
+ License-Expression: Apache-2.0
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Internet
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: System :: Distributed Computing
23
+ Classifier: Topic :: System :: Networking
24
+ Classifier: Typing :: Typed
25
+ Requires-Dist: anyio>=4.10,<5
26
+ Requires-Dist: croniter>=6.2
27
+ Requires-Dist: orjson>=3.10
28
+ Requires-Dist: pydantic>=2.12,<3
29
+ Requires-Dist: typer>=0.24.2
30
+ Requires-Dist: uvloop>=0.22.1
31
+ Requires-Dist: msgspec>=0.21 ; extra == 'msgspec'
32
+ Requires-Dist: nats-py>=2.7 ; extra == 'nats'
33
+ Requires-Dist: prometheus-client>=0.20 ; extra == 'prometheus'
34
+ Requires-Dist: redis>=6.0,<8 ; extra == 'redis'
35
+ Requires-Python: >=3.12
36
+ Project-URL: homepage, https://github.com/pyrorhythm/kuu
37
+ Provides-Extra: msgspec
38
+ Provides-Extra: nats
39
+ Provides-Extra: prometheus
40
+ Provides-Extra: redis
41
+ Description-Content-Type: text/markdown
42
+
43
+ # kuu
44
+
45
+ ```shell
46
+ uv install kuu --prerelease=allow
47
+
48
+ # extras availible: msgspec, nats, prometheus, redis
49
+ ```
50
+
51
+ a native distributed queue that is rather simple and easy-to-integrate in production
52
+ has task queue, redis xstream, nats jetstream support, result backend, middlewares and event/signals
53
+ also has a built-in scheduler yk
54
+
55
+ nothing serious in this project, just got tired of taskiq's undefined behaviour and `logging.getLogger("root")`
56
+ maybe will build a webui for it idk
@@ -0,0 +1,29 @@
1
+ from .app import Kuu
2
+ from .brokers.base import Broker, Delivery
3
+ from .context import Context
4
+ from .exceptions import NotConnected, RejectErr, RetryErr, TaskError
5
+ from .message import Message
6
+ from .middleware.base import Middleware
7
+ from .results.base import Result, ResultBackend
8
+ from .serializers import JSONSerializer, Serializer
9
+ from .handle import TaskHandle
10
+ from .task import Task
11
+
12
+ __all__ = [
13
+ "Kuu",
14
+ "Task",
15
+ "TaskHandle",
16
+ "Message",
17
+ "Context",
18
+ "RetryErr",
19
+ "RejectErr",
20
+ "TaskError",
21
+ "NotConnected",
22
+ "Middleware",
23
+ "Broker",
24
+ "Delivery",
25
+ "Result",
26
+ "ResultBackend",
27
+ "Serializer",
28
+ "JSONSerializer",
29
+ ]
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import sys
6
+ from collections.abc import Generator, Sequence
7
+ from contextlib import contextmanager
8
+ from importlib import import_module
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @contextmanager
16
+ def add_cwd_in_path() -> Generator[None]:
17
+ """
18
+ adds current directory in python path
19
+
20
+ this context manager adds current directory in sys.path,
21
+ so all python files are discoverable now, without installing
22
+ current project
23
+
24
+ :yield none
25
+ """
26
+ cwd = Path.cwd()
27
+ if str(cwd) in sys.path:
28
+ yield
29
+ else:
30
+ logger.debug(f"inserting {cwd} in sys.path")
31
+ sys.path.insert(0, str(cwd))
32
+ try:
33
+ yield
34
+ finally:
35
+ try:
36
+ sys.path.remove(str(cwd))
37
+ except ValueError:
38
+ logger.warning(f"cannot remove '{cwd}' from sys.path")
39
+
40
+
41
+ def import_object(object_spec: str, app_dir: str | None = None) -> Any:
42
+ """
43
+ it parses python object spec and imports it
44
+
45
+ :param object_spec: string in format like `package.module:variable`
46
+ :param app_dir: directory to add in sys.path for importing
47
+ :raises ValueError: if spec has unknown format
48
+ :returns imported broker:
49
+ """
50
+ import_spec = object_spec.split(":")
51
+ if len(import_spec) != 2:
52
+ raise ValueError("you should provide object path in `module:variable` format.")
53
+ with add_cwd_in_path():
54
+ if app_dir:
55
+ sys.path.insert(0, app_dir)
56
+ module = import_module(import_spec[0])
57
+ return getattr(module, import_spec[1])
58
+
59
+
60
+ def import_from_modules(modules: list[str]) -> None:
61
+ """
62
+ import all modules from modules variable
63
+
64
+ :param modules: list of modules.
65
+ """
66
+ for module in modules:
67
+ try:
68
+ logger.info(f"importing tasks from module {module}")
69
+ with add_cwd_in_path():
70
+ import_module(module)
71
+ except ImportError as err:
72
+ logger.warning(f"cannot import {module}. Cause:")
73
+ logger.exception(err)
74
+
75
+
76
+ def import_tasks(modules: list[str], pattern: str | Sequence[str], fs_discover: bool) -> None:
77
+ """
78
+ import tasks modules
79
+
80
+ this function is used to
81
+ import all tasks from modules
82
+
83
+ :param modules: list of modules to import
84
+ :param pattern: pattern of a file if fs_discover is true.
85
+ :param fs_discover: if true it will try to import modules
86
+ from filesystem.
87
+ """
88
+ if fs_discover:
89
+ if isinstance(pattern, str):
90
+ pattern = (pattern,)
91
+ discovered_modules = set()
92
+ for glob_pattern in pattern:
93
+ for path in Path().glob(glob_pattern):
94
+ if path.is_file():
95
+ if path.suffix in (".py", ".pyc", ".pyd", ".so"):
96
+ # remove all suffixes
97
+ prefix = path.name.partition(".")[0]
98
+ discovered_modules.add(
99
+ str(path.with_name(prefix)).replace(os.path.sep, ".")
100
+ )
101
+ # ignore other files
102
+ else:
103
+ discovered_modules.add(str(path).replace(os.path.sep, "."))
104
+
105
+ modules.extend(list(discovered_modules))
106
+ import_from_modules(modules)
107
+
108
+
109
+ def object_fqn(obj: object) -> str:
110
+ if hasattr(obj, "__name__"):
111
+ return f"{obj.__module__}.{obj.__name__}"
112
+ return f"{obj.__module__}.{obj.__class__.__name__}"
113
+
114
+
115
+ def get_type_fqn(arg: Any) -> str | None:
116
+ _resolved_module = ""
117
+ try:
118
+ _resolved_module = arg.__module__
119
+ except AttributeError:
120
+ if arg.__class__.__name__ in __builtins__:
121
+ _resolved_module = "builtins"
122
+
123
+ if _resolved_module == "":
124
+ return None
125
+
126
+ return _resolved_module + ":" + arg.__class__.__name__
127
+
128
+
129
+ def get_type_from_fqn(_result: str | bytes | None) -> Any:
130
+ _imported_type = None
131
+ if _result is None:
132
+ return _imported_type
133
+
134
+ _decoded_result = _result.decode() if isinstance(_result, bytes) else _result
135
+ try:
136
+ _imported_type = import_object(_decoded_result)
137
+ except Exception as exc:
138
+ logger.warning("{}", exc)
139
+
140
+ return _imported_type
@@ -0,0 +1,9 @@
1
+ from typing import TYPE_CHECKING, Callable, Coroutine
2
+
3
+ if TYPE_CHECKING:
4
+ from kuu.task import Task
5
+
6
+ type _FnAsync[**P, R] = Callable[P, Coroutine[None, None, R]]
7
+ type _Fn[**P, R] = Callable[P, R] | _FnAsync[P, R]
8
+ type _FnSingle[P, R] = Callable[[P], R]
9
+ type _Wrap[**P, R] = _FnSingle[_Fn[P, R], Task[P, R]]
kuu-0.0.1b1/kuu/app.py ADDED
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from datetime import datetime, timezone
5
+ from typing import Any, overload
6
+
7
+ from kuu._import import object_fqn
8
+ from kuu._types import _Fn, _FnAsync, _Wrap
9
+ from kuu.brokers.base import Broker
10
+ from kuu.context import Context
11
+ from kuu.events import Events
12
+ from kuu.handle import TaskHandle
13
+ from kuu.message import Message, Payload
14
+ from kuu.middleware.base import Middleware, run_chain
15
+ from kuu.registry import Registry
16
+ from kuu.results.base import ResultBackend
17
+ from kuu.serializers.base import Serializer
18
+ from kuu.serializers.json import JSONSerializer
19
+ from kuu.task import Task
20
+
21
+
22
+ class Kuu:
23
+ def __init__(
24
+ self,
25
+ broker: Broker,
26
+ default_queue: str = "default",
27
+ middleware: list[Middleware] | None = None,
28
+ results: ResultBackend | None = None,
29
+ serializer: Serializer = JSONSerializer(),
30
+ ):
31
+ self.broker = broker
32
+ self.results = results
33
+ self.serializer = serializer
34
+ self.default_queue = default_queue
35
+ self.middleware: list[Middleware] = list(middleware or [])
36
+ self.registry = Registry()
37
+ self.events = Events()
38
+
39
+ @overload
40
+ def task[**P, R](
41
+ self,
42
+ name: str | None = ...,
43
+ /,
44
+ queue: str | None = ...,
45
+ max_attempts: int = ...,
46
+ timeout: float | None = ...,
47
+ blocking: bool = ...,
48
+ ) -> _Wrap[P, R]: ...
49
+
50
+ @overload
51
+ def task[**P, R](self, func: _Fn[P, R] = ..., /) -> Task[P, R]: ...
52
+
53
+ def task[**P, R](
54
+ self,
55
+ name_or_func: str | _Fn[P, R] | None = None,
56
+ /,
57
+ queue: str | None = None,
58
+ max_attempts: int = 5,
59
+ timeout: float | None = None,
60
+ blocking: bool = False,
61
+ **labels: Any,
62
+ ) -> _Wrap[P, R] | Task[P, R]:
63
+
64
+ def _get_wrap(
65
+ _name: str | None = None,
66
+ ):
67
+ def _wrap(func: _Fn[P, R]) -> Task[P, R]:
68
+ t: Task[P, R] = Task(
69
+ manager=self,
70
+ original_func=func,
71
+ task_name=_name or object_fqn(func),
72
+ task_queue=queue or self.default_queue,
73
+ task_labels=labels,
74
+ max_attempts=max_attempts,
75
+ timeout=timeout,
76
+ blocking=blocking,
77
+ )
78
+ self.registry.add(t)
79
+ return t
80
+
81
+ return _wrap
82
+
83
+ if inspect.isfunction(name_or_func):
84
+ func = name_or_func
85
+ return _get_wrap()(func)
86
+
87
+ name = name_or_func
88
+ return _get_wrap(name)
89
+
90
+ def _build_message(
91
+ self,
92
+ task_name: str,
93
+ task: Task | None,
94
+ args: Payload,
95
+ queue: str | None,
96
+ not_before: datetime | None,
97
+ headers: dict[str, str] | None,
98
+ max_attempts: int | None,
99
+ ) -> Message:
100
+ return Message(
101
+ task=task_name,
102
+ queue=queue or (task.task_queue if task else self.default_queue),
103
+ payload=args,
104
+ headers=headers or {},
105
+ max_attempts=(
106
+ max_attempts if max_attempts is not None else (task.max_attempts if task else 5)
107
+ ),
108
+ not_before=not_before,
109
+ )
110
+
111
+ async def _dispatch(
112
+ self,
113
+ msg: Message,
114
+ task: Task[Any, Any] | None,
115
+ not_before: datetime | None,
116
+ ) -> None:
117
+ ctx = Context(app=self, message=msg, phase="enqueue", task=task)
118
+
119
+ async def _terminal(_c: Context) -> None:
120
+ if not_before is not None and not_before > datetime.now(timezone.utc):
121
+ await self.broker.schedule(msg, not_before)
122
+ else:
123
+ await self.broker.enqueue(msg)
124
+ await self.events.task_enqueued.send(msg)
125
+
126
+ await run_chain(ctx, self.middleware, _terminal)
127
+
128
+ def _enqueue_task[**P, Res](
129
+ self,
130
+ task: Task[P, Res],
131
+ queue: str | None = None,
132
+ not_before: datetime | None = None,
133
+ headers: dict[str, str] | None = None,
134
+ max_attempts: int | None = None,
135
+ ) -> _FnAsync[P, TaskHandle[Res]]:
136
+ async def _(*args: P.args, **kwargs: P.kwargs) -> TaskHandle[Res]:
137
+ payload = Payload(args=args, kwargs=kwargs)
138
+ msg = self._build_message(
139
+ task.task_name,
140
+ task,
141
+ payload,
142
+ queue=queue,
143
+ not_before=not_before,
144
+ headers=headers,
145
+ max_attempts=max_attempts,
146
+ )
147
+ await self._dispatch(msg, task, not_before)
148
+ return TaskHandle[Res](message=msg, app=self)
149
+
150
+ return _
151
+
152
+ async def enqueue_by_name(
153
+ self,
154
+ task: str,
155
+ args: Payload = Payload(),
156
+ queue: str | None = None,
157
+ not_before: datetime | None = None,
158
+ headers: dict[str, str] | None = None,
159
+ max_attempts: int | None = None,
160
+ ) -> TaskHandle[Any]:
161
+ t = self.registry.get(task)
162
+ msg = self._build_message(
163
+ task,
164
+ t,
165
+ args,
166
+ queue=queue,
167
+ not_before=not_before,
168
+ headers=headers,
169
+ max_attempts=max_attempts,
170
+ )
171
+ await self._dispatch(msg, t, not_before)
172
+ return TaskHandle[Any](message=msg, app=self)
@@ -0,0 +1,3 @@
1
+ from .base import Broker, Delivery
2
+
3
+ __all__ = ["Broker", "Delivery"]
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+ from typing import Protocol
7
+
8
+ from ..message import Message
9
+
10
+
11
+ @dataclass
12
+ class Delivery[Receipt]:
13
+ message: Message
14
+ receipt: Receipt
15
+ queue: str
16
+
17
+
18
+ class Broker(Protocol):
19
+ async def connect(self) -> None: ...
20
+ async def close(self) -> None: ...
21
+
22
+ async def declare(self, queue: str) -> None: ...
23
+
24
+ async def enqueue(self, msg: Message) -> None: ...
25
+ async def schedule(self, msg: Message, not_before: datetime) -> None: ...
26
+
27
+ def consume(self, queues: list[str], prefetch: int) -> AsyncIterator[Delivery]: ...
28
+
29
+ async def ack(self, delivery: Delivery) -> None: ...
30
+ async def nack(
31
+ self, delivery: Delivery, requeue: bool = True, delay: float | None = None
32
+ ) -> None: ...
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import heapq
4
+ import itertools
5
+ from collections.abc import AsyncIterator
6
+ from datetime import datetime, timezone
7
+ from typing import NamedTuple
8
+
9
+ import anyio
10
+ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
11
+
12
+ from ..message import Message
13
+ from .base import Broker, Delivery
14
+
15
+
16
+ class MemoryReceipt(NamedTuple):
17
+ queue: str
18
+ seq: int
19
+
20
+
21
+ class _Q(NamedTuple):
22
+ send: MemoryObjectSendStream[Delivery[MemoryReceipt]]
23
+ recv: MemoryObjectReceiveStream[Delivery[MemoryReceipt]]
24
+
25
+
26
+ class MemoryBroker(Broker):
27
+ def __init__(self, buffer: int = 1024, pump_interval: float = 0.05):
28
+ self.buffer = buffer
29
+ self.pump_interval = pump_interval
30
+ self._queues: dict[str, _Q] = {}
31
+ # (run_at_ts, seq, queue, message)
32
+ self._scheduled: list[tuple[float, int, str, Message]] = []
33
+ self._sched_lock = anyio.Lock()
34
+ self._sched_event = anyio.Event()
35
+ self._seq = itertools.count()
36
+ self._pending: dict[tuple[str, int], Message] = {}
37
+
38
+ async def connect(self) -> None:
39
+ return None
40
+
41
+ async def close(self) -> None:
42
+ for q in self._queues.values():
43
+ await q.send.aclose()
44
+ await q.recv.aclose()
45
+ self._queues.clear()
46
+ self._scheduled.clear()
47
+ self._pending.clear()
48
+
49
+ async def declare(self, queue: str) -> None:
50
+ if queue in self._queues:
51
+ return
52
+ send, recv = anyio.create_memory_object_stream[Delivery[MemoryReceipt]](self.buffer)
53
+ self._queues[queue] = _Q(send, recv)
54
+
55
+ def _now(self) -> float:
56
+ return datetime.now(timezone.utc).timestamp()
57
+
58
+ def _ts(self, dt: datetime) -> float:
59
+ return dt.replace(tzinfo=timezone.utc).timestamp() if dt.tzinfo is None else dt.timestamp()
60
+
61
+ async def _push(self, queue: str, msg: Message) -> None:
62
+ await self.declare(queue)
63
+ seq = next(self._seq)
64
+ receipt = MemoryReceipt(queue=queue, seq=seq)
65
+ self._pending[(queue, seq)] = msg
66
+ await self._queues[queue].send.send(Delivery(message=msg, receipt=receipt, queue=queue))
67
+
68
+ async def enqueue(self, msg: Message) -> None:
69
+ await self._push(msg.queue, msg)
70
+
71
+ async def schedule(self, msg: Message, not_before: datetime) -> None:
72
+ await self.declare(msg.queue)
73
+ async with self._sched_lock:
74
+ heapq.heappush(self._scheduled, (self._ts(not_before), next(self._seq), msg.queue, msg))
75
+ self._sched_event.set()
76
+ self._sched_event = anyio.Event()
77
+
78
+ async def _pump_scheduled(self) -> None:
79
+ while True:
80
+ async with self._sched_lock:
81
+ now = self._now()
82
+ due: list[tuple[str, Message]] = []
83
+ while self._scheduled and self._scheduled[0][0] <= now:
84
+ _, _, q, m = heapq.heappop(self._scheduled)
85
+ due.append((q, m))
86
+ for q, m in due:
87
+ await self._push(q, m)
88
+ await anyio.sleep(self.pump_interval)
89
+
90
+ async def consume(
91
+ self, queues: list[str], prefetch: int
92
+ ) -> AsyncIterator[Delivery[MemoryReceipt]]:
93
+ del prefetch # in-memory has its own buffer
94
+ for q in queues:
95
+ await self.declare(q)
96
+
97
+ send, recv = anyio.create_memory_object_stream[Delivery[MemoryReceipt]](self.buffer)
98
+
99
+ async def _forward(q: str) -> None:
100
+ async for delivery in self._queues[q].recv.clone():
101
+ await send.send(delivery)
102
+
103
+ async with anyio.create_task_group() as tg:
104
+ tg.start_soon(self._pump_scheduled)
105
+ for q in queues:
106
+ tg.start_soon(_forward, q)
107
+ try:
108
+ async with recv:
109
+ async for delivery in recv:
110
+ yield delivery
111
+ finally:
112
+ tg.cancel_scope.cancel()
113
+
114
+ async def ack(self, delivery: Delivery) -> None:
115
+ match delivery.receipt:
116
+ case MemoryReceipt():
117
+ pass
118
+ case _:
119
+ return
120
+ self._pending.pop((delivery.receipt.queue, delivery.receipt.seq), None)
121
+
122
+ async def nack(
123
+ self,
124
+ delivery: Delivery,
125
+ requeue: bool = True,
126
+ delay: float | None = None,
127
+ ) -> None:
128
+ match delivery.receipt:
129
+ case MemoryReceipt():
130
+ pass
131
+ case _:
132
+ return
133
+ key = (delivery.receipt.queue, delivery.receipt.seq)
134
+ self._pending.pop(key, None)
135
+ if not requeue:
136
+ return
137
+ msg = delivery.message.model_copy(update={"attempt": delivery.message.attempt + 1})
138
+ if delay and delay > 0:
139
+ when = datetime.fromtimestamp(self._now() + delay, tz=timezone.utc)
140
+ await self.schedule(msg, when)
141
+ else:
142
+ await self._push(delivery.receipt.queue, msg)