Coverage for /usr/local/lib/python3.12/site-packages/prefect/server/utilities/messaging/__init__.py: 78%
94 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 02:04 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 02:04 +0000
1import abc
2from contextlib import asynccontextmanager, AbstractAsyncContextManager
3from dataclasses import dataclass
4import importlib
5from types import TracebackType
6from typing import (
7 TYPE_CHECKING,
8 Any,
9 Callable,
10 Optional,
11 Protocol,
12 Type,
13 TypeVar,
14 Union,
15 runtime_checkable,
16)
17from collections.abc import AsyncGenerator, Awaitable, Iterable, Mapping
18from typing_extensions import Self
20from prefect.settings import PREFECT_MESSAGING_CACHE, PREFECT_MESSAGING_BROKER
21from prefect.logging import get_logger
23if TYPE_CHECKING: 23 ↛ 24line 23 didn't jump to line 24 because the condition on line 23 was never true
24 import logging
26logger: "logging.Logger" = get_logger(__name__)
29M = TypeVar("M", bound="Message", covariant=True)
32class Message(Protocol):
33 """
34 A protocol representing a message sent to a message broker.
35 """
37 @property
38 def data(self) -> Union[str, bytes]: ... 38 ↛ exitline 38 didn't return from function 'data' because
40 @property
41 def attributes(self) -> Mapping[str, Any]: ... 41 ↛ exitline 41 didn't return from function 'attributes' because
44class Cache(abc.ABC):
45 @abc.abstractmethod
46 async def clear_recently_seen_messages(self) -> None: ... 46 ↛ exitline 46 didn't return from function 'clear_recently_seen_messages' because
48 @abc.abstractmethod
49 async def without_duplicates( 49 ↛ exitline 49 didn't return from function 'without_duplicates' because
50 self, attribute: str, messages: Iterable[M]
51 ) -> list[M]: ...
53 @abc.abstractmethod
54 async def forget_duplicates( 54 ↛ exitline 54 didn't return from function 'forget_duplicates' because
55 self, attribute: str, messages: Iterable[Message]
56 ) -> None: ...
59class Publisher(AbstractAsyncContextManager["Publisher"], abc.ABC):
60 def __init__( 60 ↛ exitline 60 didn't return from function '__init__' because
61 self,
62 topic: str,
63 cache: Optional[Cache] = None,
64 deduplicate_by: Optional[str] = None,
65 ) -> None: ...
67 @abc.abstractmethod
68 async def publish_data( 68 ↛ exitline 68 didn't return from function 'publish_data' because
69 self, data: bytes, attributes: Mapping[str, str]
70 ) -> None: ...
72 @abc.abstractmethod
73 async def __aenter__(self) -> Self: ... 73 ↛ exitline 73 didn't return from function '__aenter__' because
75 @abc.abstractmethod
76 async def __aexit__( 76 ↛ exitline 76 didn't return from function '__aexit__' because
77 self,
78 exc_type: Optional[Type[BaseException]],
79 exc_val: Optional[BaseException],
80 exc_tb: Optional[TracebackType],
81 ) -> None: ...
84@dataclass
85class CapturedMessage:
86 data: bytes
87 attributes: Mapping[str, str]
90class CapturingPublisher(Publisher):
91 messages: list[CapturedMessage] = []
92 deduplicate_by: Optional[str]
94 def __init__(
95 self,
96 topic: str,
97 cache: Optional[Cache] = None,
98 deduplicate_by: Optional[str] = None,
99 ) -> None:
100 self.topic = topic
101 self.cache: Cache = cache or create_cache()
102 self.deduplicate_by = deduplicate_by
104 async def __aenter__(self) -> Self:
105 return self
107 async def __aexit__(
108 self,
109 exc_type: Optional[Type[BaseException]],
110 exc_val: Optional[BaseException],
111 exc_tb: Optional[TracebackType],
112 ) -> None:
113 pass
115 async def publish_data(self, data: bytes, attributes: Mapping[str, str]) -> None:
116 to_publish = [CapturedMessage(data, attributes)]
118 if self.deduplicate_by:
119 to_publish = await self.cache.without_duplicates(
120 self.deduplicate_by, to_publish
121 )
123 self.messages.extend(to_publish)
126MessageHandler = Callable[[Message], Awaitable[None]]
129class StopConsumer(Exception):
130 """
131 Exception to raise to stop a consumer.
132 """
134 def __init__(self, ack: bool = False):
135 self.ack = ack
138class Consumer(abc.ABC):
139 """
140 Abstract base class for consumers that receive messages from a message broker and
141 call a handler function for each message received.
142 """
144 def __init__(self, topic: str, **kwargs: Any) -> None:
145 self.topic = topic
147 @abc.abstractmethod
148 async def run(self, handler: MessageHandler) -> None:
149 """Runs the consumer (indefinitely)"""
150 ...
152 async def cleanup(self) -> None:
153 """Cleanup resources when the consumer is stopped.
155 Override this method in subclasses that need to perform cleanup,
156 such as unsubscribing from topics or closing connections.
158 The default implementation is a no-op, which is appropriate for
159 consumers that don't need explicit cleanup.
160 """
161 pass
164@runtime_checkable
165class CacheModule(Protocol):
166 Cache: type[Cache]
169def create_cache() -> Cache:
170 """
171 Creates a new cache with the applications default settings.
173 Returns:
174 a new Cache instance
175 """
176 module = importlib.import_module(PREFECT_MESSAGING_CACHE.value())
177 assert isinstance(module, CacheModule)
179 return module.Cache()
182@runtime_checkable
183class BrokerModule(Protocol):
184 Publisher: type[Publisher]
185 Consumer: type[Consumer]
186 ephemeral_subscription: Callable[
187 [str], AbstractAsyncContextManager[Mapping[str, Any]]
188 ]
190 # Used for testing: a context manager that breaks the topic in a way that raises
191 # a ValueError("oops") when attempting to publish a message.
192 break_topic: Callable[[], AbstractAsyncContextManager[None]]
195def create_publisher(
196 topic: str, cache: Optional[Cache] = None, deduplicate_by: Optional[str] = None
197) -> Publisher:
198 """
199 Creates a new publisher with the applications default settings.
200 Args:
201 topic: the topic to publish to
202 Returns:
203 a new Consumer instance
204 """
205 cache = cache or create_cache()
207 module = importlib.import_module(PREFECT_MESSAGING_BROKER.value())
208 assert isinstance(module, BrokerModule)
209 return module.Publisher(topic, cache, deduplicate_by=deduplicate_by)
212@asynccontextmanager
213async def ephemeral_subscription(topic: str) -> AsyncGenerator[Mapping[str, Any], Any]:
214 """
215 Creates an ephemeral subscription to the given source, removing it when the context
216 exits.
217 """
218 module = importlib.import_module(PREFECT_MESSAGING_BROKER.value())
219 assert isinstance(module, BrokerModule)
220 async with module.ephemeral_subscription(topic) as consumer_create_kwargs:
221 yield consumer_create_kwargs
224def create_consumer(topic: str, **kwargs: Any) -> Consumer:
225 """
226 Creates a new consumer with the applications default settings.
227 Args:
228 topic: the topic to consume from
229 Returns:
230 a new Consumer instance
231 """
232 module = importlib.import_module(PREFECT_MESSAGING_BROKER.value())
233 assert isinstance(module, BrokerModule)
234 return module.Consumer(topic, **kwargs)