Skip to content

Commit 1bcbcf5

Browse files
authored
feat: add RMQ channels options, support for prefix for routing_key, a… (#1448)
* feat: add RMQ channels options, support for prefix for routing_key, add public API for middlewares * tests: fix asyncapi tests * chore: update dependencies * fix: parse old NATS stream config if it exists * feat (#1447): add StreamMessage.batch_headers attr to provide access to whole batch messages headers * fix: add factory is_flag option * feat: add batch_headers for Confluent
1 parent d100d5f commit 1bcbcf5

File tree

31 files changed

+589
-96
lines changed

31 files changed

+589
-96
lines changed

faststream/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Simple and fast framework to create message brokers based microservices."""
22

3-
__version__ = "0.5.5"
3+
__version__ = "0.5.6"
44

55
SERVICE_NAME = f"faststream-{__version__}"
66

faststream/broker/core/abc.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ def __init__(
4646
self._parser = parser
4747
self._decoder = decoder
4848

49+
def add_middleware(self, middleware: "BrokerMiddleware[MsgType]") -> None:
50+
"""Append BrokerMiddleware to the end of middlewares list.
51+
52+
Current middleware will be used as a most inner of already existed ones.
53+
"""
54+
self._middlewares = (*self._middlewares, middleware)
55+
56+
for sub in self._subscribers.values():
57+
sub.add_middleware(middleware)
58+
59+
for pub in self._publishers.values():
60+
pub.add_middleware(middleware)
61+
4962
@abstractmethod
5063
def subscriber(
5164
self,

faststream/broker/message.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
TYPE_CHECKING,
77
Any,
88
Generic,
9+
List,
910
Optional,
1011
Sequence,
1112
Tuple,
@@ -38,6 +39,7 @@ class StreamMessage(Generic[MsgType]):
3839

3940
body: Union[bytes, Any]
4041
headers: "AnyDict" = field(default_factory=dict)
42+
batch_headers: List["AnyDict"] = field(default_factory=list)
4143
path: "AnyDict" = field(default_factory=dict)
4244

4345
content_type: Optional[str] = None

faststream/broker/publisher/proto.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ class PublisherProto(
5656
_middlewares: Iterable["PublisherMiddleware"]
5757
_producer: Optional["ProducerProto"]
5858

59+
@abstractmethod
60+
def add_middleware(self, middleware: "BrokerMiddleware[MsgType]") -> None: ...
61+
5962
@staticmethod
6063
@abstractmethod
6164
def create() -> "PublisherProto[MsgType]":

faststream/broker/publisher/usecase.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@
1919
from faststream.asyncapi.message import get_response_schema
2020
from faststream.asyncapi.utils import to_camelcase
2121
from faststream.broker.publisher.proto import PublisherProto
22-
from faststream.broker.types import MsgType, P_HandlerParams, T_HandlerReturn
22+
from faststream.broker.types import (
23+
BrokerMiddleware,
24+
MsgType,
25+
P_HandlerParams,
26+
T_HandlerReturn,
27+
)
2328
from faststream.broker.wrapper.call import HandlerCallWrapper
2429

2530
if TYPE_CHECKING:
@@ -87,6 +92,9 @@ def __init__(
8792
self.include_in_schema = include_in_schema
8893
self.schema_ = schema_
8994

95+
def add_middleware(self, middleware: "BrokerMiddleware[MsgType]") -> None:
96+
self._broker_middlewares = (*self._broker_middlewares, middleware)
97+
9098
@override
9199
def setup( # type: ignore[override]
92100
self,

faststream/broker/subscriber/proto.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ class SubscriberProto(
3535
_broker_middlewares: Iterable["BrokerMiddleware[MsgType]"]
3636
_producer: Optional["ProducerProto"]
3737

38+
@abstractmethod
39+
def add_middleware(self, middleware: "BrokerMiddleware[MsgType]") -> None: ...
40+
3841
@staticmethod
3942
@abstractmethod
4043
def create() -> "SubscriberProto[MsgType]":

faststream/broker/subscriber/usecase.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ def __init__(
131131
self.description_ = description_
132132
self.include_in_schema = include_in_schema
133133

134+
def add_middleware(self, middleware: "BrokerMiddleware[MsgType]") -> None:
135+
self._broker_middlewares = (*self._broker_middlewares, middleware)
136+
134137
@override
135138
def setup( # type: ignore[override]
136139
self,

faststream/cli/docs/app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ def serve(
4545
),
4646
),
4747
is_factory: bool = typer.Option(
48-
False,
49-
"--factory", help="Treat APP as an application factory"
48+
False, "--factory", help="Treat APP as an application factory"
5049
),
5150
) -> None:
5251
"""Serve project AsyncAPI schema."""
@@ -110,7 +109,8 @@ def gen(
110109
),
111110
is_factory: bool = typer.Option(
112111
False,
113-
"--factory", help="Treat APP as an application factory"
112+
"--factory",
113+
help="Treat APP as an application factory",
114114
),
115115
) -> None:
116116
"""Generate project AsyncAPI schema."""

faststream/cli/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,9 @@ def publish(
211211
rpc: bool = typer.Option(False, help="Enable RPC mode and system output"),
212212
is_factory: bool = typer.Option(
213213
False,
214-
"--factory", help="Treat APP as an application factory"
214+
"--factory",
215+
is_flag=True,
216+
help="Treat APP as an application factory",
215217
),
216218
) -> None:
217219
"""Publish a message using the specified broker in a FastStream application.

faststream/confluent/parser.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING, Any, Optional, Tuple
1+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Union
22

33
from faststream.broker.message import decode_message, gen_cor_id
44
from faststream.confluent.message import FAKE_CONSUMER, KafkaMessage
@@ -20,18 +20,14 @@ async def parse_message(
2020
message: "Message",
2121
) -> "StreamMessage[Message]":
2222
"""Parses a Kafka message."""
23-
headers = {}
24-
if message.headers() is not None:
25-
for i, j in message.headers(): # type: ignore[union-attr]
26-
if isinstance(j, str):
27-
headers[i] = j
28-
else:
29-
headers[i] = j.decode()
23+
headers = _parse_msg_headers(message.headers())
24+
3025
body = message.value()
3126
offset = message.offset()
3227
_, timestamp = message.timestamp()
3328

3429
handler: Optional["LogicSubscriber[Any]"] = context.get_local("handler_")
30+
3531
return KafkaMessage(
3632
body=body,
3733
headers=headers,
@@ -49,28 +45,29 @@ async def parse_message_batch(
4945
message: Tuple["Message", ...],
5046
) -> "StreamMessage[Tuple[Message, ...]]":
5147
"""Parses a batch of messages from a Kafka consumer."""
48+
body: List[Any] = []
49+
batch_headers: List[Dict[str, str]] = []
50+
5251
first = message[0]
5352
last = message[-1]
5453

55-
headers = {}
56-
if first.headers() is not None:
57-
for i, j in first.headers(): # type: ignore[union-attr]
58-
if isinstance(j, str):
59-
headers[i] = j
60-
else:
61-
headers[i] = j.decode()
62-
body = [m.value() for m in message]
63-
first_offset = first.offset()
64-
last_offset = last.offset()
54+
for m in message:
55+
body.append(m.value)
56+
batch_headers.append(_parse_msg_headers(m.headers()))
57+
58+
headers = next(iter(batch_headers), {})
59+
6560
_, first_timestamp = first.timestamp()
6661

6762
handler: Optional["LogicSubscriber[Any]"] = context.get_local("handler_")
63+
6864
return KafkaMessage(
6965
body=body,
7066
headers=headers,
67+
batch_headers=batch_headers,
7168
reply_to=headers.get("reply_to", ""),
7269
content_type=headers.get("content-type"),
73-
message_id=f"{first_offset}-{last_offset}-{first_timestamp}",
70+
message_id=f"{first.offset()}-{last.offset()}-{first_timestamp}",
7471
correlation_id=headers.get("correlation_id", gen_cor_id()),
7572
raw_message=message,
7673
consumer=getattr(handler, "consumer", None) or FAKE_CONSUMER,
@@ -91,3 +88,9 @@ async def decode_message_batch(
9188
) -> "DecodedMessage":
9289
"""Decode a batch of messages."""
9390
return [decode_message(await cls.parse_message(m)) for m in msg.raw_message]
91+
92+
93+
def _parse_msg_headers(
94+
headers: Sequence[Tuple[str, Union[bytes, str]]],
95+
) -> Dict[str, str]:
96+
return {i: j if isinstance(j, str) else j.decode() for i, j in headers}

faststream/kafka/parser.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING, Any, Optional, Tuple
1+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
22

33
from faststream.broker.message import decode_message, gen_cor_id
44
from faststream.kafka.message import FAKE_CONSUMER, KafkaMessage
@@ -39,13 +39,24 @@ async def parse_message_batch(
3939
message: Tuple["ConsumerRecord", ...],
4040
) -> "StreamMessage[Tuple[ConsumerRecord, ...]]":
4141
"""Parses a batch of messages from a Kafka consumer."""
42+
body: List[Any] = []
43+
batch_headers: List[Dict[str, str]] = []
44+
4245
first = message[0]
4346
last = message[-1]
44-
headers = {i: j.decode() for i, j in first.headers}
47+
48+
for m in message:
49+
body.append(m.value)
50+
batch_headers.append({i: j.decode() for i, j in m.headers})
51+
52+
headers = next(iter(batch_headers), {})
53+
4554
handler: Optional["LogicSubscriber[Any]"] = context.get_local("handler_")
55+
4656
return KafkaMessage(
47-
body=[m.value for m in message],
57+
body=body,
4858
headers=headers,
59+
batch_headers=batch_headers,
4960
reply_to=headers.get("reply_to", ""),
5061
content_type=headers.get("content-type"),
5162
message_id=f"{first.offset}-{last.offset}-{first.timestamp}",

faststream/nats/broker/broker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -623,12 +623,12 @@ async def start(self) -> None:
623623
)
624624

625625
except BadRequestError as e:
626-
old_config = (await self.stream.stream_info(stream.name)).config
627-
628626
if (
629627
e.description
630628
== "stream name already in use with a different configuration"
631629
):
630+
old_config = (await self.stream.stream_info(stream.name)).config
631+
632632
self._log(str(e), logging.WARNING, log_context)
633633
await self.stream.update_stream(
634634
config=stream.config,

faststream/nats/parser.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING, List, Optional
1+
from typing import TYPE_CHECKING, Dict, List, Optional
22

33
from faststream.broker.message import StreamMessage, decode_message, gen_cor_id
44
from faststream.nats.message import NatsBatchMessage, NatsMessage
@@ -102,15 +102,27 @@ async def parse_batch(
102102
self,
103103
message: List["Msg"],
104104
) -> "StreamMessage[List[Msg]]":
105-
if first_msg := next(iter(message), None):
106-
path = self.get_path(first_msg.subject)
105+
body: List[bytes] = []
106+
batch_headers: List[Dict[str, str]] = []
107+
108+
if message:
109+
path = self.get_path(message[0].subject)
110+
111+
for m in message:
112+
batch_headers.append(m.headers or {})
113+
body.append(m.data)
114+
107115
else:
108116
path = None
109117

118+
headers = next(iter(batch_headers), {})
119+
110120
return NatsBatchMessage(
111121
raw_message=message,
112-
body=[m.data for m in message],
122+
body=body,
113123
path=path or {},
124+
headers=headers,
125+
batch_headers=batch_headers,
114126
)
115127

116128
async def decode_batch(

faststream/rabbit/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@
2121
"ReplyConfig",
2222
"RabbitExchange",
2323
"RabbitQueue",
24+
# Annotations
2425
"RabbitMessage",
2526
)

faststream/rabbit/annotations.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from aio_pika import RobustChannel, RobustConnection
12
from typing_extensions import Annotated
23

34
from faststream.annotations import ContextRepo, Logger, NoCast
@@ -13,8 +14,20 @@
1314
"RabbitMessage",
1415
"RabbitBroker",
1516
"RabbitProducer",
17+
"Channel",
18+
"Connection",
1619
)
1720

1821
RabbitMessage = Annotated[RM, Context("message")]
1922
RabbitBroker = Annotated[RB, Context("broker")]
2023
RabbitProducer = Annotated[AioPikaFastProducer, Context("broker._producer")]
24+
25+
Channel = Annotated[RobustChannel, Context("broker._channel")]
26+
Connection = Annotated[RobustConnection, Context("broker._connection")]
27+
28+
# NOTE: transaction is not for the public usage yet
29+
# async def _get_transaction(connection: Connection) -> RabbitTransaction:
30+
# async with connection.channel(publisher_confirms=False) as channel:
31+
# yield channel.transaction()
32+
33+
# Transaction = Annotated[RabbitTransaction, Depends(_get_transaction)]

0 commit comments

Comments
 (0)