diff --git a/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_info.py b/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_info.py index 4121bebe29..d177e86909 100644 --- a/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_info.py +++ b/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_info.py @@ -1,7 +1,6 @@ from faststream import FastStream from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.license import License -from faststream.specification.schema.contact import Contact +from faststream.specification import License, Contact from faststream.kafka import KafkaBroker broker = KafkaBroker("localhost:9092") diff --git a/faststream/_internal/_compat.py b/faststream/_internal/_compat.py index f58233757d..ba38326ac9 100644 --- a/faststream/_internal/_compat.py +++ b/faststream/_internal/_compat.py @@ -110,7 +110,7 @@ def model_to_jsonable( def dump_json(data: Any) -> bytes: return json_dumps(model_to_jsonable(data)) - def get_model_fields(model: type[BaseModel]) -> dict[str, Any]: + def get_model_fields(model: type[BaseModel]) -> AnyDict: return model.model_fields def model_to_json(model: BaseModel, **kwargs: Any) -> str: @@ -140,7 +140,7 @@ def model_schema(model: type[BaseModel], **kwargs: Any) -> AnyDict: def dump_json(data: Any) -> bytes: return json_dumps(data, default=pydantic_encoder) - def get_model_fields(model: type[BaseModel]) -> dict[str, Any]: + def get_model_fields(model: type[BaseModel]) -> AnyDict: return model.__fields__ # type: ignore[return-value] def model_to_json(model: BaseModel, **kwargs: Any) -> str: @@ -187,7 +187,6 @@ def with_info_plain_validator_function( # type: ignore[misc] ExceptionGroup, ) - try: import email_validator diff --git a/faststream/_internal/basic_types.py b/faststream/_internal/basic_types.py index f781df146e..e844171150 100644 --- a/faststream/_internal/basic_types.py +++ b/faststream/_internal/basic_types.py @@ -58,7 +58,7 @@ class StandardDataclass(Protocol): """Protocol to check type is dataclass.""" - __dataclass_fields__: ClassVar[dict[str, Any]] + __dataclass_fields__: ClassVar[AnyDict] BaseSendableMessage: TypeAlias = Union[ diff --git a/faststream/_internal/broker/broker.py b/faststream/_internal/broker/broker.py index b3621d34ee..831295ae76 100644 --- a/faststream/_internal/broker/broker.py +++ b/faststream/_internal/broker/broker.py @@ -35,6 +35,7 @@ MsgType, ) from faststream._internal.utils.functions import to_async +from faststream.specification.proto import ServerSpecification from .abc_broker import ABCBroker from .pub_base import BrokerPublishMixin @@ -51,12 +52,13 @@ PublisherProto, ) from faststream.security import BaseSecurity - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict class BrokerUsecase( ABCBroker[MsgType], SetupAble, + ServerSpecification, BrokerPublishMixin[MsgType], Generic[MsgType, ConnectionType], ): @@ -121,7 +123,7 @@ def __init__( Doc("AsyncAPI server description."), ], tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), ], specification_url: Annotated[ diff --git a/faststream/_internal/cli/docs/app.py b/faststream/_internal/cli/docs/app.py index d85f53de9c..d7c7b5951d 100644 --- a/faststream/_internal/cli/docs/app.py +++ b/faststream/_internal/cli/docs/app.py @@ -12,8 +12,12 @@ from faststream._internal.cli.utils.imports import import_from_string from faststream.exceptions import INSTALL_WATCHFILES, INSTALL_YAML, SCHEMA_NOT_SUPPORTED from faststream.specification.asyncapi.site import serve_app -from faststream.specification.asyncapi.v2_6_0.schema import Schema as SchemaV2_6 -from faststream.specification.asyncapi.v3_0_0.schema import Schema as SchemaV3 +from faststream.specification.asyncapi.v2_6_0.schema import ( + ApplicationSchema as SchemaV2_6, +) +from faststream.specification.asyncapi.v3_0_0.schema import ( + ApplicationSchema as SchemaV3, +) from faststream.specification.base.specification import Specification if TYPE_CHECKING: diff --git a/faststream/_internal/cli/utils/imports.py b/faststream/_internal/cli/utils/imports.py index 27be43cf05..860b69a42a 100644 --- a/faststream/_internal/cli/utils/imports.py +++ b/faststream/_internal/cli/utils/imports.py @@ -8,7 +8,9 @@ def import_from_string( - import_str: str, *, is_factory: bool = False + import_str: str, + *, + is_factory: bool = False, ) -> tuple[Path, object]: module_path, instance = _import_object_or_factory(import_str) diff --git a/faststream/_internal/fastapi/router.py b/faststream/_internal/fastapi/router.py index 4828893324..29452792b1 100644 --- a/faststream/_internal/fastapi/router.py +++ b/faststream/_internal/fastapi/router.py @@ -55,7 +55,7 @@ from faststream._internal.types import BrokerMiddleware from faststream.message import StreamMessage from faststream.specification.base.specification import Specification - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict class _BackgroundMiddleware(BaseMiddleware): @@ -121,7 +121,7 @@ def __init__( generate_unique_id, ), # Specification information - specification_tags: Optional[Iterable[Union["Tag", "TagDict"]]] = None, + specification_tags: Iterable[Union["Tag", "TagDict"]] = (), schema_url: Optional[str] = "/asyncapi", **connection_kwars: Any, ) -> None: diff --git a/faststream/_internal/publisher/specified.py b/faststream/_internal/publisher/specified.py index 8ad62a1d00..a6e34a163b 100644 --- a/faststream/_internal/publisher/specified.py +++ b/faststream/_internal/publisher/specified.py @@ -1,11 +1,9 @@ from inspect import Parameter, unwrap -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Callable, Optional, Union from fast_depends.core import build_call_model from fast_depends.pydantic._compat import create_model, get_config_base -from faststream._internal.publisher.proto import PublisherProto -from faststream._internal.subscriber.call_wrapper.call import HandlerCallWrapper from faststream._internal.types import ( MsgType, P_HandlerParams, @@ -13,34 +11,40 @@ ) from faststream.specification.asyncapi.message import get_model_schema from faststream.specification.asyncapi.utils import to_camelcase -from faststream.specification.base.proto import SpecificationEndpoint +from faststream.specification.proto import EndpointSpecification +from faststream.specification.schema import PublisherSpec if TYPE_CHECKING: - from faststream._internal.basic_types import AnyDict + from faststream._internal.basic_types import AnyCallable, AnyDict + from faststream._internal.state import BrokerState, Pointer + from faststream._internal.subscriber.call_wrapper.call import HandlerCallWrapper -class BaseSpicificationPublisher(SpecificationEndpoint, PublisherProto[MsgType]): +class SpecificationPublisher(EndpointSpecification[PublisherSpec]): """A base class for publishers in an asynchronous API.""" + _state: "Pointer[BrokerState]" # should be set in next parent + def __init__( self, - *, + *args: Any, schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, + **kwargs: Any, ) -> None: - self.calls = [] + self.calls: list[AnyCallable] = [] - self.title_ = title_ - self.description_ = description_ - self.include_in_schema = include_in_schema self.schema_ = schema_ + super().__init__(*args, **kwargs) + def __call__( self, - func: HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn], - ) -> HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]: + func: Union[ + Callable[P_HandlerParams, T_HandlerReturn], + "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", + ], + ) -> "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]": + func = super().__call__(func) self.calls.append(func._original_call) return func diff --git a/faststream/_internal/publisher/usecase.py b/faststream/_internal/publisher/usecase.py index c729a5b13d..46ebf0f7da 100644 --- a/faststream/_internal/publisher/usecase.py +++ b/faststream/_internal/publisher/usecase.py @@ -27,8 +27,6 @@ ) from faststream.message.source_type import SourceType -from .specified import BaseSpicificationPublisher - if TYPE_CHECKING: from faststream._internal.publisher.proto import ProducerProto from faststream._internal.types import ( @@ -38,7 +36,7 @@ from faststream.response.response import PublishCommand -class PublisherUsecase(BaseSpicificationPublisher, PublisherProto[MsgType]): +class PublisherUsecase(PublisherProto[MsgType]): """A base class for publishers in an asynchronous API.""" def __init__( @@ -46,11 +44,6 @@ def __init__( *, broker_middlewares: Iterable["BrokerMiddleware[MsgType]"], middlewares: Iterable["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: self.middlewares = middlewares self._broker_middlewares = broker_middlewares @@ -60,13 +53,6 @@ def __init__( self._fake_handler = False self.mock: Optional[MagicMock] = None - super().__init__( - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - schema_=schema_, - ) - self._state: Pointer[BrokerState] = Pointer( EmptyBrokerState("You should include publisher to any broker.") ) @@ -115,7 +101,6 @@ def __call__( ensure_call_wrapper(func) ) handler._publishers.append(self) - super().__call__(handler) return handler async def _basic_publish( diff --git a/faststream/_internal/subscriber/proto.py b/faststream/_internal/subscriber/proto.py index a402009407..8b6151df30 100644 --- a/faststream/_internal/subscriber/proto.py +++ b/faststream/_internal/subscriber/proto.py @@ -17,7 +17,6 @@ ProducerProto, ) from faststream._internal.state import BrokerState, Pointer - from faststream._internal.subscriber.call_item import HandlerItem from faststream._internal.types import ( BrokerMiddleware, CustomCallable, @@ -27,6 +26,8 @@ from faststream.message import StreamMessage from faststream.response import Response + from .call_item import HandlerItem + class SubscriberProto( Endpoint, @@ -68,10 +69,6 @@ def _make_response_publisher( message: "StreamMessage[MsgType]", ) -> Iterable["BasePublisherProto"]: ... - @property - @abstractmethod - def call_name(self) -> str: ... - @abstractmethod async def start(self) -> None: ... diff --git a/faststream/_internal/subscriber/specified.py b/faststream/_internal/subscriber/specified.py index e6dec70970..3af87b590c 100644 --- a/faststream/_internal/subscriber/specified.py +++ b/faststream/_internal/subscriber/specified.py @@ -1,30 +1,38 @@ from typing import ( TYPE_CHECKING, + Any, Optional, ) -from faststream._internal.subscriber.proto import SubscriberProto -from faststream._internal.types import MsgType from faststream.exceptions import SetupError from faststream.specification.asyncapi.message import parse_handler_params from faststream.specification.asyncapi.utils import to_camelcase -from faststream.specification.base.proto import SpecificationEndpoint +from faststream.specification.proto import EndpointSpecification +from faststream.specification.schema import SubscriberSpec if TYPE_CHECKING: from faststream._internal.basic_types import AnyDict + from faststream._internal.types import ( + MsgType, + ) + from .call_item import HandlerItem + + +class SpecificationSubscriber( + EndpointSpecification[SubscriberSpec], +): + calls: list["HandlerItem[MsgType]"] -class BaseSpicificationSubscriber(SpecificationEndpoint, SubscriberProto[MsgType]): def __init__( self, - *, - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, + *args: Any, + **kwargs: Any, ) -> None: - self.title_ = title_ - self.description_ = description_ - self.include_in_schema = include_in_schema + self.calls = [] + + # Call next base class parent init + super().__init__(*args, **kwargs) @property def call_name(self) -> str: @@ -34,9 +42,9 @@ def call_name(self) -> str: return to_camelcase(self.calls[0].call_name) - def get_description(self) -> Optional[str]: + def get_default_description(self) -> Optional[str]: """Returns the description of the handler.""" - if not self.calls: # pragma: no cover + if not self.calls: return None return self.calls[0].description diff --git a/faststream/_internal/subscriber/usecase.py b/faststream/_internal/subscriber/usecase.py index c8ee25678a..3a8aa1227d 100644 --- a/faststream/_internal/subscriber/usecase.py +++ b/faststream/_internal/subscriber/usecase.py @@ -34,8 +34,6 @@ from faststream.middlewares.logging import CriticalLogMiddleware from faststream.response import ensure_response -from .specified import BaseSpicificationSubscriber - if TYPE_CHECKING: from fast_depends.dependencies import Dependant @@ -79,7 +77,7 @@ def __init__( self.dependencies = dependencies -class SubscriberUsecase(BaseSpicificationSubscriber, SubscriberProto[MsgType]): +class SubscriberUsecase(SubscriberProto[MsgType]): """A class representing an asynchronous handler.""" lock: "AbstractContextManager[Any]" @@ -100,18 +98,8 @@ def __init__( default_parser: "AsyncCallable", default_decoder: "AsyncCallable", ack_policy: AckPolicy, - # AsyncAPI information - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: """Initialize a new instance of the class.""" - super().__init__( - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - self.calls = [] self._parser = default_parser diff --git a/faststream/_internal/utils/data.py b/faststream/_internal/utils/data.py index cc12c4cec2..98e3729fac 100644 --- a/faststream/_internal/utils/data.py +++ b/faststream/_internal/utils/data.py @@ -5,8 +5,19 @@ TypedDictCls = TypeVar("TypedDictCls") -def filter_by_dict(typed_dict: type[TypedDictCls], data: AnyDict) -> TypedDictCls: +def filter_by_dict( + typed_dict: type[TypedDictCls], + data: AnyDict, +) -> tuple[TypedDictCls, AnyDict]: annotations = typed_dict.__annotations__ - return typed_dict( # type: ignore[call-arg] - {k: v for k, v in data.items() if k in annotations}, - ) + + out_data = {} + extra_data = {} + + for k, v in data.items(): + if k in annotations: + out_data[k] = v + else: + extra_data[k] = v + + return typed_dict(out_data), extra_data diff --git a/faststream/confluent/broker/broker.py b/faststream/confluent/broker/broker.py index d8a1cd6671..53b487b031 100644 --- a/faststream/confluent/broker/broker.py +++ b/faststream/confluent/broker/broker.py @@ -53,7 +53,7 @@ from faststream.confluent.config import ConfluentConfig from faststream.confluent.message import KafkaMessage from faststream.security import BaseSecurity - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict Partition = TypeVar("Partition") @@ -304,9 +304,9 @@ def __init__( Doc("AsyncAPI server description."), ] = None, tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], @@ -452,9 +452,10 @@ async def _connect( # type: ignore[override] self._producer.connect(native_producer) + connection_kwargs, _ = filter_by_dict(ConsumerConnectionParams, kwargs) return partial( AsyncConfluentConsumer, - **filter_by_dict(ConsumerConnectionParams, kwargs), + **connection_kwargs, logger=self._state.get().logger_state, config=self.config, ) diff --git a/faststream/confluent/fastapi/fastapi.py b/faststream/confluent/fastapi/fastapi.py index 5bdc96cf6c..197aa380af 100644 --- a/faststream/confluent/fastapi/fastapi.py +++ b/faststream/confluent/fastapi/fastapi.py @@ -53,7 +53,7 @@ SpecificationDefaultSubscriber, ) from faststream.security import BaseSecurity - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict Partition = TypeVar("Partition") @@ -296,9 +296,9 @@ def __init__( Doc("Specification server description."), ] = None, specification_tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("Specification server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], diff --git a/faststream/confluent/publisher/specified.py b/faststream/confluent/publisher/specified.py index fec0faf183..69b4ca499b 100644 --- a/faststream/confluent/publisher/specified.py +++ b/faststream/confluent/publisher/specified.py @@ -1,58 +1,43 @@ -from typing import ( - TYPE_CHECKING, -) - -from faststream._internal.types import MsgType -from faststream.confluent.publisher.usecase import ( - BatchPublisher, - DefaultPublisher, - LogicPublisher, +from faststream._internal.publisher.specified import ( + SpecificationPublisher as SpecificationPublisherMixin, ) +from faststream.confluent.publisher.usecase import BatchPublisher, DefaultPublisher from faststream.specification.asyncapi.utils import resolve_payloads +from faststream.specification.schema import Message, Operation, PublisherSpec from faststream.specification.schema.bindings import ChannelBinding, kafka -from faststream.specification.schema.channel import Channel -from faststream.specification.schema.message import CorrelationId, Message -from faststream.specification.schema.operation import Operation -if TYPE_CHECKING: - from confluent_kafka import Message as ConfluentMsg - -class SpecificationPublisher(LogicPublisher[MsgType]): +class SpecificationPublisher(SpecificationPublisherMixin): """A class representing a publisher.""" - def get_name(self) -> str: + def get_default_name(self) -> str: return f"{self.topic}:Publisher" - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, PublisherSpec]: payloads = self.get_payloads() return { - self.name: Channel( + self.name: PublisherSpec( description=self.description, - publish=Operation( + operation=Operation( message=Message( title=f"{self.name}:Message", payload=resolve_payloads(payloads, "Publisher"), - correlationId=CorrelationId( - location="$message.header#/correlation_id", - ), ), + bindings=None, + ), + bindings=ChannelBinding( + kafka=kafka.ChannelBinding( + topic=self.topic, partitions=None, replicas=None + ) ), - bindings=ChannelBinding(kafka=kafka.ChannelBinding(topic=self.topic)), ), } -class SpecificationBatchPublisher( - BatchPublisher, - SpecificationPublisher[tuple["ConfluentMsg", ...]], -): +class SpecificationBatchPublisher(SpecificationPublisher, BatchPublisher): pass -class SpecificationDefaultPublisher( - DefaultPublisher, - SpecificationPublisher["ConfluentMsg"], -): +class SpecificationDefaultPublisher(SpecificationPublisher, DefaultPublisher): pass diff --git a/faststream/confluent/publisher/usecase.py b/faststream/confluent/publisher/usecase.py index d6b7132155..e7cd4a1fb4 100644 --- a/faststream/confluent/publisher/usecase.py +++ b/faststream/confluent/publisher/usecase.py @@ -1,7 +1,6 @@ from collections.abc import Iterable from typing import ( TYPE_CHECKING, - Any, Optional, Union, ) @@ -40,20 +39,10 @@ def __init__( # Publisher args broker_middlewares: Iterable["BrokerMiddleware[MsgType]"], middlewares: Iterable["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( broker_middlewares=broker_middlewares, middlewares=middlewares, - # AsyncAPI args - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.topic = topic @@ -105,11 +94,6 @@ def __init__( # Publisher args broker_middlewares: Iterable["BrokerMiddleware[Message]"], middlewares: Iterable["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( topic=topic, @@ -119,11 +103,6 @@ def __init__( # publisher args broker_middlewares=broker_middlewares, middlewares=middlewares, - # AsyncAPI args - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.key = key diff --git a/faststream/confluent/subscriber/specified.py b/faststream/confluent/subscriber/specified.py index ece2b78457..dbb20b4f7a 100644 --- a/faststream/confluent/subscriber/specified.py +++ b/faststream/confluent/subscriber/specified.py @@ -1,64 +1,54 @@ -from typing import ( - TYPE_CHECKING, -) +from collections.abc import Iterable +from typing import TYPE_CHECKING -from faststream._internal.types import MsgType -from faststream.confluent.subscriber.usecase import ( - BatchSubscriber, - DefaultSubscriber, - LogicSubscriber, +from faststream._internal.subscriber.specified import ( + SpecificationSubscriber as SpecificationSubscriberMixin, ) +from faststream.confluent.subscriber.usecase import BatchSubscriber, DefaultSubscriber from faststream.specification.asyncapi.utils import resolve_payloads +from faststream.specification.schema import Message, Operation, SubscriberSpec from faststream.specification.schema.bindings import ChannelBinding, kafka -from faststream.specification.schema.channel import Channel -from faststream.specification.schema.message import CorrelationId, Message -from faststream.specification.schema.operation import Operation if TYPE_CHECKING: - from confluent_kafka import Message as ConfluentMsg + from faststream.confluent.schemas import TopicPartition -class SpecificationSubscriber(LogicSubscriber[MsgType]): +class SpecificationSubscriber(SpecificationSubscriberMixin): """A class to handle logic and async API operations.""" - def get_name(self) -> str: + topics: Iterable[str] + partitions: Iterable["TopicPartition"] # TODO: support partitions + + def get_default_name(self) -> str: return f"{','.join(self.topics)}:{self.call_name}" - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, SubscriberSpec]: channels = {} payloads = self.get_payloads() for t in self.topics: handler_name = self.title_ or f"{t}:{self.call_name}" - channels[handler_name] = Channel( + channels[handler_name] = SubscriberSpec( description=self.description, - subscribe=Operation( + operation=Operation( message=Message( title=f"{handler_name}:Message", payload=resolve_payloads(payloads), - correlationId=CorrelationId( - location="$message.header#/correlation_id", - ), ), + bindings=None, ), bindings=ChannelBinding( - kafka=kafka.ChannelBinding(topic=t), + kafka=kafka.ChannelBinding(topic=t, partitions=None, replicas=None), ), ) return channels -class SpecificationDefaultSubscriber( - DefaultSubscriber, - SpecificationSubscriber["ConfluentMsg"], -): +class SpecificationDefaultSubscriber(SpecificationSubscriber, DefaultSubscriber): pass -class SpecificationBatchSubscriber( - BatchSubscriber, - SpecificationSubscriber[tuple["ConfluentMsg", ...]], -): +class SpecificationBatchSubscriber(SpecificationSubscriber, BatchSubscriber): pass diff --git a/faststream/confluent/subscriber/usecase.py b/faststream/confluent/subscriber/usecase.py index adb321dd4a..d1963d7ab4 100644 --- a/faststream/confluent/subscriber/usecase.py +++ b/faststream/confluent/subscriber/usecase.py @@ -63,10 +63,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[MsgType]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( default_parser=default_parser, @@ -76,10 +72,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.__connection_data = connection_data @@ -258,10 +250,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[Message]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: self.parser = AsyncConfluentParser(is_manual=is_manual) @@ -279,10 +267,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) async def get_msg(self) -> Optional["Message"]: @@ -321,10 +305,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[tuple[Message, ...]]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: self.max_records = max_records @@ -344,10 +324,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) async def get_msg(self) -> Optional[tuple["Message", ...]]: diff --git a/faststream/exceptions.py b/faststream/exceptions.py index 6d51e76cb3..32557dd42c 100644 --- a/faststream/exceptions.py +++ b/faststream/exceptions.py @@ -162,4 +162,4 @@ def __str__(self) -> str: pip install watchfiles """ -SCHEMA_NOT_SUPPORTED = "{schema_filename} not supported. Make sure that your schema is valid and schema version supported by FastStream" +SCHEMA_NOT_SUPPORTED = "`{schema_filename}` not supported. Make sure that your schema is valid and schema version supported by FastStream" diff --git a/faststream/kafka/broker/broker.py b/faststream/kafka/broker/broker.py index 0f962f0d3b..bec2ed99c9 100644 --- a/faststream/kafka/broker/broker.py +++ b/faststream/kafka/broker/broker.py @@ -57,7 +57,7 @@ ) from faststream.kafka.message import KafkaMessage from faststream.security import BaseSecurity - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict class KafkaInitKwargs(TypedDict, total=False): request_timeout_ms: Annotated[ @@ -477,9 +477,9 @@ def __init__( Doc("AsyncAPI server description."), ] = None, tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], @@ -640,10 +640,8 @@ async def _connect( # type: ignore[override] await self._producer.connect(producer) - return partial( - aiokafka.AIOKafkaConsumer, - **filter_by_dict(ConsumerConnectionParams, kwargs), - ) + connection_kwargs, _ = filter_by_dict(ConsumerConnectionParams, kwargs) + return partial(aiokafka.AIOKafkaConsumer, **connection_kwargs) async def start(self) -> None: """Connect broker to Kafka and startup all subscribers.""" diff --git a/faststream/kafka/fastapi/fastapi.py b/faststream/kafka/fastapi/fastapi.py index 5601c05f6c..46a07fb7a7 100644 --- a/faststream/kafka/fastapi/fastapi.py +++ b/faststream/kafka/fastapi/fastapi.py @@ -58,7 +58,7 @@ SpecificationDefaultSubscriber, ) from faststream.security import BaseSecurity - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict Partition = TypeVar("Partition") @@ -304,9 +304,9 @@ def __init__( Doc("Specification server description."), ] = None, specification_tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("Specification server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], diff --git a/faststream/kafka/publisher/specified.py b/faststream/kafka/publisher/specified.py index d765cc8f8b..b23eef8d92 100644 --- a/faststream/kafka/publisher/specified.py +++ b/faststream/kafka/publisher/specified.py @@ -1,56 +1,43 @@ -from typing import TYPE_CHECKING - -from faststream._internal.types import MsgType -from faststream.kafka.publisher.usecase import ( - BatchPublisher, - DefaultPublisher, - LogicPublisher, +from faststream._internal.publisher.specified import ( + SpecificationPublisher as SpecificationPublisherMixin, ) +from faststream.kafka.publisher.usecase import BatchPublisher, DefaultPublisher from faststream.specification.asyncapi.utils import resolve_payloads +from faststream.specification.schema import Message, Operation, PublisherSpec from faststream.specification.schema.bindings import ChannelBinding, kafka -from faststream.specification.schema.channel import Channel -from faststream.specification.schema.message import CorrelationId, Message -from faststream.specification.schema.operation import Operation - -if TYPE_CHECKING: - from aiokafka import ConsumerRecord -class SpecificationPublisher(LogicPublisher[MsgType]): +class SpecificationPublisher(SpecificationPublisherMixin): """A class representing a publisher.""" - def get_name(self) -> str: + def get_default_name(self) -> str: return f"{self.topic}:Publisher" - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, PublisherSpec]: payloads = self.get_payloads() return { - self.name: Channel( + self.name: PublisherSpec( description=self.description, - publish=Operation( + operation=Operation( message=Message( title=f"{self.name}:Message", payload=resolve_payloads(payloads, "Publisher"), - correlationId=CorrelationId( - location="$message.header#/correlation_id", - ), ), + bindings=None, + ), + bindings=ChannelBinding( + kafka=kafka.ChannelBinding( + topic=self.topic, partitions=None, replicas=None + ) ), - bindings=ChannelBinding(kafka=kafka.ChannelBinding(topic=self.topic)), ), } -class SpecificationBatchPublisher( - BatchPublisher, - SpecificationPublisher[tuple["ConsumerRecord", ...]], -): +class SpecificationBatchPublisher(SpecificationPublisher, BatchPublisher): pass -class SpecificationDefaultPublisher( - DefaultPublisher, - SpecificationPublisher["ConsumerRecord"], -): +class SpecificationDefaultPublisher(SpecificationPublisher, DefaultPublisher): pass diff --git a/faststream/kafka/publisher/usecase.py b/faststream/kafka/publisher/usecase.py index 0f005770de..895abe044c 100644 --- a/faststream/kafka/publisher/usecase.py +++ b/faststream/kafka/publisher/usecase.py @@ -42,20 +42,10 @@ def __init__( # Publisher args broker_middlewares: Iterable["BrokerMiddleware[MsgType]"], middlewares: Iterable["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( broker_middlewares=broker_middlewares, middlewares=middlewares, - # AsyncAPI args - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.topic = topic @@ -154,11 +144,6 @@ def __init__( # Publisher args broker_middlewares: Iterable["BrokerMiddleware[ConsumerRecord]"], middlewares: Iterable["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( topic=topic, @@ -168,11 +153,6 @@ def __init__( # publisher args broker_middlewares=broker_middlewares, middlewares=middlewares, - # AsyncAPI args - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.key = key diff --git a/faststream/kafka/subscriber/specified.py b/faststream/kafka/subscriber/specified.py index c06856f518..a536e54143 100644 --- a/faststream/kafka/subscriber/specified.py +++ b/faststream/kafka/subscriber/specified.py @@ -1,30 +1,29 @@ -from typing import ( - TYPE_CHECKING, -) +from collections.abc import Iterable +from typing import TYPE_CHECKING, Optional -from faststream._internal.types import MsgType -from faststream.kafka.subscriber.usecase import ( - BatchSubscriber, - DefaultSubscriber, - LogicSubscriber, +from faststream._internal.subscriber.specified import ( + SpecificationSubscriber as SpecificationSubscriberMixin, ) +from faststream.kafka.subscriber.usecase import BatchSubscriber, DefaultSubscriber from faststream.specification.asyncapi.utils import resolve_payloads +from faststream.specification.schema import Message, Operation, SubscriberSpec from faststream.specification.schema.bindings import ChannelBinding, kafka -from faststream.specification.schema.channel import Channel -from faststream.specification.schema.message import CorrelationId, Message -from faststream.specification.schema.operation import Operation if TYPE_CHECKING: - from aiokafka import ConsumerRecord + from aiokafka import TopicPartition -class SpecificationSubscriber(LogicSubscriber[MsgType]): +class SpecificationSubscriber(SpecificationSubscriberMixin): """A class to handle logic and async API operations.""" - def get_name(self) -> str: + topics: Iterable[str] + partitions: Iterable["TopicPartition"] # TODO: support partitions + _pattern: Optional[str] # TODO: support pattern schema + + def get_default_name(self) -> str: return f"{','.join(self.topics)}:{self.call_name}" - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, SubscriberSpec]: channels = {} payloads = self.get_payloads() @@ -32,34 +31,26 @@ def get_schema(self) -> dict[str, Channel]: for t in self.topics: handler_name = self.title_ or f"{t}:{self.call_name}" - channels[handler_name] = Channel( + channels[handler_name] = SubscriberSpec( description=self.description, - subscribe=Operation( + operation=Operation( message=Message( title=f"{handler_name}:Message", payload=resolve_payloads(payloads), - correlationId=CorrelationId( - location="$message.header#/correlation_id", - ), ), + bindings=None, ), bindings=ChannelBinding( - kafka=kafka.ChannelBinding(topic=t), + kafka=kafka.ChannelBinding(topic=t, partitions=None, replicas=None), ), ) return channels -class SpecificationDefaultSubscriber( - DefaultSubscriber, - SpecificationSubscriber["ConsumerRecord"], -): +class SpecificationDefaultSubscriber(SpecificationSubscriber, DefaultSubscriber): pass -class SpecificationBatchSubscriber( - BatchSubscriber, - SpecificationSubscriber[tuple["ConsumerRecord", ...]], -): +class SpecificationBatchSubscriber(SpecificationSubscriber, BatchSubscriber): pass diff --git a/faststream/kafka/subscriber/usecase.py b/faststream/kafka/subscriber/usecase.py index fab52a66f2..3fc89600e2 100644 --- a/faststream/kafka/subscriber/usecase.py +++ b/faststream/kafka/subscriber/usecase.py @@ -69,10 +69,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[MsgType]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( default_parser=default_parser, @@ -82,10 +78,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.topics = topics @@ -287,10 +279,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[ConsumerRecord]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: if pattern: reg, pattern = compile_path( @@ -322,10 +310,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) async def get_msg(self) -> "ConsumerRecord": @@ -368,10 +352,6 @@ def __init__( broker_middlewares: Iterable[ "BrokerMiddleware[Sequence[tuple[ConsumerRecord, ...]]]" ], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: self.batch_timeout_ms = batch_timeout_ms self.max_records = max_records @@ -406,10 +386,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) async def get_msg(self) -> tuple["ConsumerRecord", ...]: diff --git a/faststream/nats/broker/broker.py b/faststream/nats/broker/broker.py index b5996e29e8..8cfa07a9bd 100644 --- a/faststream/nats/broker/broker.py +++ b/faststream/nats/broker/broker.py @@ -36,7 +36,7 @@ from faststream.nats.publisher.producer import NatsFastProducer, NatsJSFastProducer from faststream.nats.response import NatsPublishCommand from faststream.nats.security import parse_security -from faststream.nats.subscriber.specified import SpecificationSubscriber +from faststream.nats.subscriber.usecases.basic import LogicSubscriber from faststream.response.publish_type import PublishType from .logging import make_nats_logger_state @@ -71,9 +71,9 @@ CustomCallable, ) from faststream.nats.message import NatsMessage - from faststream.nats.publisher.specified import SpecificationPublisher + from faststream.nats.publisher.usecase import LogicPublisher from faststream.security import BaseSecurity - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict class NatsInitKwargs(TypedDict, total=False): """NatsBroker.connect() method type hints.""" @@ -399,9 +399,9 @@ def __init__( Doc("AsyncAPI server description."), ] = None, tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], @@ -560,7 +560,6 @@ async def _connect(self, **kwargs: Any) -> "Client": self._os_declarer.connect(stream) self._connection_state = ConnectedState(connection, stream) - return connection async def close( @@ -600,7 +599,7 @@ async def start(self) -> None: ) except BadRequestError as e: # noqa: PERF203 - log_context = SpecificationSubscriber.build_log_context( + log_context = LogicSubscriber.build_log_context( message=None, subject="", queue="", @@ -773,7 +772,7 @@ async def request( # type: ignore[override] @override def setup_subscriber( # type: ignore[override] self, - subscriber: "SpecificationSubscriber", + subscriber: "LogicSubscriber", ) -> None: return super().setup_subscriber( subscriber, @@ -785,7 +784,7 @@ def setup_subscriber( # type: ignore[override] @override def setup_publisher( # type: ignore[override] self, - publisher: "SpecificationPublisher", + publisher: "LogicPublisher", ) -> None: producer = self._js_producer if publisher.stream is not None else self._producer @@ -851,7 +850,7 @@ def _log_connection_broken( self, error_cb: Optional["ErrorCallback"] = None, ) -> "ErrorCallback": - c = SpecificationSubscriber.build_log_context(None, "") + c = LogicSubscriber.build_log_context(None, "") async def wrapper(err: Exception) -> None: if error_cb is not None: @@ -872,7 +871,7 @@ def _log_reconnected( self, cb: Optional["Callback"] = None, ) -> "Callback": - c = SpecificationSubscriber.build_log_context(None, "") + c = LogicSubscriber.build_log_context(None, "") async def wrapper() -> None: if cb is not None: diff --git a/faststream/nats/fastapi/fastapi.py b/faststream/nats/fastapi/fastapi.py index a62318c82b..3c465c783c 100644 --- a/faststream/nats/fastapi/fastapi.py +++ b/faststream/nats/fastapi/fastapi.py @@ -63,7 +63,7 @@ from faststream.nats.publisher.specified import SpecificationPublisher from faststream.nats.schemas import JStream, KvWatch, ObjWatch, PullSub from faststream.security import BaseSecurity - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict class NatsRouter(StreamRouter["Msg"]): @@ -245,9 +245,9 @@ def __init__( Doc("AsyncAPI server description."), ] = None, specification_tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], diff --git a/faststream/nats/publisher/specified.py b/faststream/nats/publisher/specified.py index 41cfdc27b9..029c62b344 100644 --- a/faststream/nats/publisher/specified.py +++ b/faststream/nats/publisher/specified.py @@ -1,35 +1,38 @@ +from faststream._internal.publisher.specified import ( + SpecificationPublisher as SpecificationPublisherMixin, +) from faststream.nats.publisher.usecase import LogicPublisher from faststream.specification.asyncapi.utils import resolve_payloads +from faststream.specification.schema import Message, Operation, PublisherSpec from faststream.specification.schema.bindings import ChannelBinding, nats -from faststream.specification.schema.channel import Channel -from faststream.specification.schema.message import CorrelationId, Message -from faststream.specification.schema.operation import Operation -class SpecificationPublisher(LogicPublisher): +class SpecificationPublisher( + SpecificationPublisherMixin, + LogicPublisher, +): """A class to represent a NATS publisher.""" - def get_name(self) -> str: + def get_default_name(self) -> str: return f"{self.subject}:Publisher" - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, PublisherSpec]: payloads = self.get_payloads() return { - self.name: Channel( + self.name: PublisherSpec( description=self.description, - publish=Operation( + operation=Operation( message=Message( title=f"{self.name}:Message", payload=resolve_payloads(payloads, "Publisher"), - correlationId=CorrelationId( - location="$message.header#/correlation_id", - ), ), + bindings=None, ), bindings=ChannelBinding( nats=nats.ChannelBinding( subject=self.subject, + queue=None, ), ), ), diff --git a/faststream/nats/publisher/usecase.py b/faststream/nats/publisher/usecase.py index 9d3ccd92dc..2b16854d73 100644 --- a/faststream/nats/publisher/usecase.py +++ b/faststream/nats/publisher/usecase.py @@ -1,7 +1,6 @@ from collections.abc import Iterable from typing import ( TYPE_CHECKING, - Any, Optional, Union, ) @@ -41,21 +40,11 @@ def __init__( # Publisher args broker_middlewares: Iterable["BrokerMiddleware[Msg]"], middlewares: Iterable["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: """Initialize NATS publisher object.""" super().__init__( broker_middlewares=broker_middlewares, middlewares=middlewares, - # AsyncAPI args - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.subject = subject diff --git a/faststream/nats/subscriber/specified.py b/faststream/nats/subscriber/specified.py index 2c3387ded3..dc8c6720ef 100644 --- a/faststream/nats/subscriber/specified.py +++ b/faststream/nats/subscriber/specified.py @@ -1,7 +1,8 @@ -from typing import Any - from typing_extensions import override +from faststream._internal.subscriber.specified import ( + SpecificationSubscriber as SpecificationSubscriberMixin, +) from faststream.nats.subscriber.usecases import ( BatchPullStreamSubscriber, ConcurrentCoreSubscriber, @@ -9,38 +10,35 @@ ConcurrentPushStreamSubscriber, CoreSubscriber, KeyValueWatchSubscriber, - LogicSubscriber, ObjStoreWatchSubscriber, PullStreamSubscriber, PushStreamSubscription, ) from faststream.specification.asyncapi.utils import resolve_payloads +from faststream.specification.schema import Message, Operation, SubscriberSpec from faststream.specification.schema.bindings import ChannelBinding, nats -from faststream.specification.schema.channel import Channel -from faststream.specification.schema.message import CorrelationId, Message -from faststream.specification.schema.operation import Operation -class SpecificationSubscriber(LogicSubscriber[Any]): +class SpecificationSubscriber(SpecificationSubscriberMixin): """A class to represent a NATS handler.""" - def get_name(self) -> str: + subject: str + + def get_default_name(self) -> str: return f"{self.subject}:{self.call_name}" - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, SubscriberSpec]: payloads = self.get_payloads() return { - self.name: Channel( + self.name: SubscriberSpec( description=self.description, - subscribe=Operation( + operation=Operation( message=Message( title=f"{self.name}:Message", payload=resolve_payloads(payloads), - correlationId=CorrelationId( - location="$message.header#/correlation_id", - ), ), + bindings=None, ), bindings=ChannelBinding( nats=nats.ChannelBinding( @@ -108,11 +106,11 @@ class SpecificationKeyValueWatchSubscriber( """KeyValueWatch consumer with Specification methods.""" @override - def get_name(self) -> str: + def get_default_name(self) -> str: return "" @override - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, SubscriberSpec]: return {} @@ -123,9 +121,9 @@ class SpecificationObjStoreWatchSubscriber( """ObjStoreWatch consumer with Specification methods.""" @override - def get_name(self) -> str: + def get_default_name(self) -> str: return "" @override - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, SubscriberSpec]: return {} diff --git a/faststream/nats/subscriber/usecase.py b/faststream/nats/subscriber/usecase.py index e2c6b207e1..8c85b8569a 100644 --- a/faststream/nats/subscriber/usecase.py +++ b/faststream/nats/subscriber/usecase.py @@ -6,7 +6,6 @@ Annotated, Any, Callable, - Generic, Optional, cast, ) @@ -68,7 +67,7 @@ from faststream.nats.schemas import JStream, KvWatch, ObjWatch, PullSub -class LogicSubscriber(SubscriberUsecase[MsgType], Generic[MsgType]): +class LogicSubscriber(SubscriberUsecase[MsgType]): """A class to represent a NATS handler.""" subscription: Optional[Unsubscriptable] diff --git a/faststream/nats/subscriber/usecases/basic.py b/faststream/nats/subscriber/usecases/basic.py index 3b5f30e1fc..bee03746b3 100644 --- a/faststream/nats/subscriber/usecases/basic.py +++ b/faststream/nats/subscriber/usecases/basic.py @@ -3,7 +3,6 @@ from typing import ( TYPE_CHECKING, Any, - Generic, Optional, ) @@ -46,7 +45,7 @@ from faststream.nats.helpers import KVBucketDeclarer, OSBucketDeclarer -class LogicSubscriber(SubscriberUsecase[MsgType], Generic[MsgType]): +class LogicSubscriber(SubscriberUsecase[MsgType]): """Basic class for all NATS Subscriber types (KeyValue, ObjectStorage, Core & JetStream).""" subscription: Optional[Unsubscriptable] @@ -66,10 +65,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[MsgType]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: self.subject = subject self.config = config @@ -84,10 +79,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self._fetch_sub = None @@ -201,10 +192,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[MsgType]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( subject=subject, @@ -218,10 +205,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, ) def _make_response_publisher( diff --git a/faststream/nats/subscriber/usecases/core_subscriber.py b/faststream/nats/subscriber/usecases/core_subscriber.py index 3cff6547d2..e8d2b6a045 100644 --- a/faststream/nats/subscriber/usecases/core_subscriber.py +++ b/faststream/nats/subscriber/usecases/core_subscriber.py @@ -43,10 +43,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: parser_ = NatsParser(pattern=subject) @@ -64,10 +60,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, ) @override @@ -148,10 +140,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( max_workers=max_workers, @@ -164,10 +152,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, ) @override diff --git a/faststream/nats/subscriber/usecases/key_value_subscriber.py b/faststream/nats/subscriber/usecases/key_value_subscriber.py index cf4a2a3f4e..9b3f27c494 100644 --- a/faststream/nats/subscriber/usecases/key_value_subscriber.py +++ b/faststream/nats/subscriber/usecases/key_value_subscriber.py @@ -52,10 +52,6 @@ def __init__( kv_watch: "KvWatch", broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[KeyValue.Entry]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: parser = KvParser(pattern=subject) self.kv_watch = kv_watch @@ -70,10 +66,6 @@ def __init__( default_decoder=parser.decode_message, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, ) @override diff --git a/faststream/nats/subscriber/usecases/object_storage_subscriber.py b/faststream/nats/subscriber/usecases/object_storage_subscriber.py index a1d5bace48..0e6332ce3e 100644 --- a/faststream/nats/subscriber/usecases/object_storage_subscriber.py +++ b/faststream/nats/subscriber/usecases/object_storage_subscriber.py @@ -56,10 +56,6 @@ def __init__( obj_watch: "ObjWatch", broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[list[Msg]]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: parser = ObjParser(pattern="") @@ -76,10 +72,6 @@ def __init__( default_decoder=parser.decode_message, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, ) @override diff --git a/faststream/nats/subscriber/usecases/stream_basic.py b/faststream/nats/subscriber/usecases/stream_basic.py index c053f2ce5e..80de14d278 100644 --- a/faststream/nats/subscriber/usecases/stream_basic.py +++ b/faststream/nats/subscriber/usecases/stream_basic.py @@ -50,10 +50,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: parser_ = JsParser(pattern=subject) @@ -72,10 +68,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, ) def get_log_context( diff --git a/faststream/nats/subscriber/usecases/stream_pull_subscriber.py b/faststream/nats/subscriber/usecases/stream_pull_subscriber.py index 44d82e89dd..7fa638eb11 100644 --- a/faststream/nats/subscriber/usecases/stream_pull_subscriber.py +++ b/faststream/nats/subscriber/usecases/stream_pull_subscriber.py @@ -58,10 +58,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: self.pull_sub = pull_sub @@ -77,10 +73,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, ) @override @@ -136,10 +128,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( max_workers=max_workers, @@ -154,10 +142,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, ) @override @@ -199,10 +183,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[list[Msg]]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: parser = BatchParser(pattern=subject) @@ -221,10 +201,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, ) @override diff --git a/faststream/nats/subscriber/usecases/stream_push_subscriber.py b/faststream/nats/subscriber/usecases/stream_push_subscriber.py index ac14ae3509..66ea31c68d 100644 --- a/faststream/nats/subscriber/usecases/stream_push_subscriber.py +++ b/faststream/nats/subscriber/usecases/stream_push_subscriber.py @@ -65,10 +65,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( max_workers=max_workers, @@ -83,10 +79,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, ) @override diff --git a/faststream/rabbit/broker/broker.py b/faststream/rabbit/broker/broker.py index b0e60e62cf..7b7e585829 100644 --- a/faststream/rabbit/broker/broker.py +++ b/faststream/rabbit/broker/broker.py @@ -59,7 +59,7 @@ from faststream.rabbit.message import RabbitMessage from faststream.rabbit.types import AioPikaSendableMessage from faststream.security import BaseSecurity - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict class RabbitBroker( @@ -196,9 +196,9 @@ def __init__( Doc("AsyncAPI server description."), ] = None, tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], diff --git a/faststream/rabbit/fastapi/fastapi.py b/faststream/rabbit/fastapi/fastapi.py index 6e32718447..02d2d4b2e9 100644 --- a/faststream/rabbit/fastapi/fastapi.py +++ b/faststream/rabbit/fastapi/fastapi.py @@ -50,7 +50,7 @@ from faststream.rabbit.message import RabbitMessage from faststream.rabbit.publisher.specified import SpecificationPublisher from faststream.security import BaseSecurity - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict class RabbitRouter(StreamRouter["IncomingMessage"]): @@ -176,9 +176,9 @@ def __init__( Doc("AsyncAPI server description."), ] = None, specification_tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], diff --git a/faststream/rabbit/publisher/specified.py b/faststream/rabbit/publisher/specified.py index 6d16769926..e8da19b3fd 100644 --- a/faststream/rabbit/publisher/specified.py +++ b/faststream/rabbit/publisher/specified.py @@ -1,34 +1,77 @@ +from collections.abc import Iterable +from typing import ( + TYPE_CHECKING, + Any, + Optional, +) + +from faststream._internal.publisher.specified import ( + SpecificationPublisher as SpecificationPublisherMixin, +) +from faststream.rabbit.schemas.proto import BaseRMQInformation as RMQSpecificationMixin from faststream.rabbit.utils import is_routing_exchange from faststream.specification.asyncapi.utils import resolve_payloads +from faststream.specification.schema import Message, Operation, PublisherSpec from faststream.specification.schema.bindings import ( ChannelBinding, OperationBinding, amqp, ) -from faststream.specification.schema.channel import Channel -from faststream.specification.schema.message import CorrelationId, Message -from faststream.specification.schema.operation import Operation -from .usecase import LogicPublisher +from .usecase import LogicPublisher, PublishKwargs +if TYPE_CHECKING: + from aio_pika import IncomingMessage -class SpecificationPublisher(LogicPublisher): - """AsyncAPI-compatible Rabbit Publisher class. + from faststream._internal.types import BrokerMiddleware, PublisherMiddleware + from faststream.rabbit.schemas import RabbitExchange, RabbitQueue - Creating by - ```python - publisher: SpecificationPublisher = ( - broker.publisher(...) - ) - # or - publisher: SpecificationPublisher = ( - router.publisher(...) - ) - ``` - """ +class SpecificationPublisher( + SpecificationPublisherMixin, + RMQSpecificationMixin, + LogicPublisher, +): + """AsyncAPI-compatible Rabbit Publisher class.""" - def get_name(self) -> str: + def __init__( + self, + *, + routing_key: str, + queue: "RabbitQueue", + exchange: "RabbitExchange", + # PublishCommand options + message_kwargs: "PublishKwargs", + # Publisher args + broker_middlewares: Iterable["BrokerMiddleware[IncomingMessage]"], + middlewares: Iterable["PublisherMiddleware"], + # AsyncAPI args + schema_: Optional[Any], + title_: Optional[str], + description_: Optional[str], + include_in_schema: bool, + ) -> None: + super().__init__( + title_=title_, + description_=description_, + include_in_schema=include_in_schema, + schema_=schema_, + # propagate to RMQSpecificationMixin + queue=queue, + exchange=exchange, + ) + + LogicPublisher.__init__( + self, + queue=queue, + exchange=exchange, + routing_key=routing_key, + message_kwargs=message_kwargs, + middlewares=middlewares, + broker_middlewares=broker_middlewares, + ) + + def get_default_name(self) -> str: routing = ( self.routing_key or (self.queue.routing if is_routing_exchange(self.exchange) else None) @@ -37,26 +80,28 @@ def get_name(self) -> str: return f"{routing}:{getattr(self.exchange, 'name', None) or '_'}:Publisher" - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, PublisherSpec]: payloads = self.get_payloads() + exchange_binding = amqp.Exchange.from_exchange(self.exchange) + queue_binding = amqp.Queue.from_queue(self.queue) + return { - self.name: Channel( + self.name: PublisherSpec( description=self.description, - publish=Operation( + operation=Operation( bindings=OperationBinding( amqp=amqp.OperationBinding( - cc=self.routing or None, - deliveryMode=2 - if self.message_options.get("persist") - else 1, - replyTo=self.message_options.get("reply_to"), # type: ignore[arg-type] - mandatory=self.publish_options.get("mandatory"), # type: ignore[arg-type] - priority=self.message_options.get("priority"), # type: ignore[arg-type] + routing_key=self.routing or None, + queue=queue_binding, + exchange=exchange_binding, + ack=True, + persist=self.message_options.get("persist"), + priority=self.message_options.get("priority"), + reply_to=self.message_options.get("reply_to"), + mandatory=self.publish_options.get("mandatory"), ), - ) - if is_routing_exchange(self.exchange) - else None, + ), message=Message( title=f"{self.name}:Message", payload=resolve_payloads( @@ -64,34 +109,13 @@ def get_schema(self) -> dict[str, Channel]: "Publisher", served_words=2 if self.title_ is None else 1, ), - correlationId=CorrelationId( - location="$message.header#/correlation_id", - ), ), ), bindings=ChannelBinding( amqp=amqp.ChannelBinding( - is_="routingKey", - queue=amqp.Queue( - name=self.queue.name, - durable=self.queue.durable, - exclusive=self.queue.exclusive, - autoDelete=self.queue.auto_delete, - vhost=self.virtual_host, - ) - if is_routing_exchange(self.exchange) and self.queue.name - else None, - exchange=( - amqp.Exchange(type="default", vhost=self.virtual_host) - if not self.exchange.name - else amqp.Exchange( - type=self.exchange.type.value, - name=self.exchange.name, - durable=self.exchange.durable, - autoDelete=self.exchange.auto_delete, - vhost=self.virtual_host, - ) - ), + virtual_host=self.virtual_host, + queue=queue_binding, + exchange=exchange_binding, ), ), ), diff --git a/faststream/rabbit/publisher/usecase.py b/faststream/rabbit/publisher/usecase.py index f34cf06b3c..0ae13cf319 100644 --- a/faststream/rabbit/publisher/usecase.py +++ b/faststream/rabbit/publisher/usecase.py @@ -3,7 +3,6 @@ from typing import ( TYPE_CHECKING, Annotated, - Any, Optional, Union, ) @@ -15,7 +14,7 @@ from faststream._internal.utils.data import filter_by_dict from faststream.message import gen_cor_id from faststream.rabbit.response import RabbitPublishCommand -from faststream.rabbit.schemas import BaseRMQInformation, RabbitExchange, RabbitQueue +from faststream.rabbit.schemas import RabbitExchange, RabbitQueue from faststream.response.publish_type import PublishType from .options import MessageOptions, PublishOptions @@ -47,10 +46,7 @@ class PublishKwargs(MessageOptions, PublishOptions, total=False): ] -class LogicPublisher( - PublisherUsecase[IncomingMessage], - BaseRMQInformation, -): +class LogicPublisher(PublisherUsecase[IncomingMessage]): """A class to represent a RabbitMQ publisher.""" app_id: Optional[str] @@ -68,53 +64,38 @@ def __init__( # Publisher args broker_middlewares: Iterable["BrokerMiddleware[IncomingMessage]"], middlewares: Iterable["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: + self.queue = queue + self.routing_key = routing_key + + self.exchange = exchange + super().__init__( broker_middlewares=broker_middlewares, middlewares=middlewares, - # AsyncAPI args - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) - self.routing_key = routing_key - request_options = dict(message_kwargs) self.headers = request_options.pop("headers") or {} self.reply_to = request_options.pop("reply_to", "") self.timeout = request_options.pop("timeout", None) - self.message_options = filter_by_dict(MessageOptions, request_options) - self.publish_options = filter_by_dict(PublishOptions, request_options) - # BaseRMQInformation - self.queue = queue - self.exchange = exchange + message_options, _ = filter_by_dict(MessageOptions, request_options) + self.message_options = message_options + + publish_options, _ = filter_by_dict(PublishOptions, request_options) + self.publish_options = publish_options - # Setup it later self.app_id = None - self.virtual_host = "" @override def _setup( # type: ignore[override] self, *, - app_id: Optional[str], - virtual_host: str, state: "BrokerState", ) -> None: - if app_id: - self.message_options["app_id"] = app_id - self.app_id = app_id - - self.virtual_host = virtual_host - + # AppId was set in `faststream.rabbit.schemas.proto.BaseRMQInformation` + self.message_options["app_id"] = self.app_id super()._setup(state=state) @property diff --git a/faststream/rabbit/schemas/proto.py b/faststream/rabbit/schemas/proto.py index 41045b94fa..2109772124 100644 --- a/faststream/rabbit/schemas/proto.py +++ b/faststream/rabbit/schemas/proto.py @@ -1,13 +1,40 @@ -from typing import Optional, Protocol +from typing import TYPE_CHECKING, Any, Optional -from faststream.rabbit.schemas.exchange import RabbitExchange -from faststream.rabbit.schemas.queue import RabbitQueue +if TYPE_CHECKING: + from faststream.rabbit.schemas.exchange import RabbitExchange + from faststream.rabbit.schemas.queue import RabbitQueue -class BaseRMQInformation(Protocol): +class BaseRMQInformation: """Base class to store Specification RMQ bindings.""" virtual_host: str - queue: RabbitQueue - exchange: RabbitExchange + queue: "RabbitQueue" + exchange: "RabbitExchange" app_id: Optional[str] + + def __init__( + self, + *, + queue: "RabbitQueue", + exchange: "RabbitExchange", + ) -> None: + self.queue = queue + self.exchange = exchange + + # Setup it later + self.app_id = None + self.virtual_host = "" + + def _setup( + self, + *, + app_id: Optional[str], + virtual_host: str, + **kwargs: Any, + ) -> None: + self.app_id = app_id + self.virtual_host = virtual_host + + # Setup next parent class + super()._setup(**kwargs) diff --git a/faststream/rabbit/subscriber/specified.py b/faststream/rabbit/subscriber/specified.py index 275f509d2b..b071ea2828 100644 --- a/faststream/rabbit/subscriber/specified.py +++ b/faststream/rabbit/subscriber/specified.py @@ -1,67 +1,107 @@ +from collections.abc import Iterable +from typing import TYPE_CHECKING, Optional + +from faststream._internal.subscriber.specified import ( + SpecificationSubscriber as SpecificationSubscriberMixin, +) +from faststream.rabbit.schemas.proto import BaseRMQInformation as RMQSpecificationMixin from faststream.rabbit.subscriber.usecase import LogicSubscriber -from faststream.rabbit.utils import is_routing_exchange from faststream.specification.asyncapi.utils import resolve_payloads +from faststream.specification.schema import Message, Operation, SubscriberSpec from faststream.specification.schema.bindings import ( ChannelBinding, OperationBinding, amqp, ) -from faststream.specification.schema.channel import Channel -from faststream.specification.schema.message import CorrelationId, Message -from faststream.specification.schema.operation import Operation +if TYPE_CHECKING: + from aio_pika import IncomingMessage + from fast_depends.dependencies import Dependant + + from faststream._internal.basic_types import AnyDict + from faststream._internal.types import BrokerMiddleware + from faststream.middlewares import AckPolicy + from faststream.rabbit.schemas.exchange import RabbitExchange + from faststream.rabbit.schemas.queue import RabbitQueue -class SpecificationSubscriber(LogicSubscriber): + +class SpecificationSubscriber( + SpecificationSubscriberMixin, + RMQSpecificationMixin, + LogicSubscriber, +): """AsyncAPI-compatible Rabbit Subscriber class.""" - def get_name(self) -> str: + def __init__( + self, + *, + queue: "RabbitQueue", + exchange: "RabbitExchange", + consume_args: Optional["AnyDict"], + # Subscriber args + ack_policy: "AckPolicy", + no_reply: bool, + broker_dependencies: Iterable["Dependant"], + broker_middlewares: Iterable["BrokerMiddleware[IncomingMessage]"], + # AsyncAPI args + title_: Optional[str], + description_: Optional[str], + include_in_schema: bool, + ) -> None: + super().__init__( + title_=title_, + description_=description_, + include_in_schema=include_in_schema, + # propagate to RMQSpecificationMixin + queue=queue, + exchange=exchange, + ) + + LogicSubscriber.__init__( + self, + queue=queue, + consume_args=consume_args, + ack_policy=ack_policy, + no_reply=no_reply, + broker_dependencies=broker_dependencies, + broker_middlewares=broker_middlewares, + ) + + def get_default_name(self) -> str: return f"{self.queue.name}:{getattr(self.exchange, 'name', None) or '_'}:{self.call_name}" - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, SubscriberSpec]: payloads = self.get_payloads() + exchange_binding = amqp.Exchange.from_exchange(self.exchange) + queue_binding = amqp.Queue.from_queue(self.queue) + return { - self.name: Channel( + self.name: SubscriberSpec( description=self.description, - subscribe=Operation( + operation=Operation( bindings=OperationBinding( amqp=amqp.OperationBinding( - cc=self.queue.routing, + routing_key=self.queue.routing, + queue=queue_binding, + exchange=exchange_binding, + ack=True, + reply_to=None, + persist=None, + mandatory=None, + priority=None, ), - ) - if is_routing_exchange(self.exchange) - else None, + ), message=Message( title=f"{self.name}:Message", payload=resolve_payloads(payloads), - correlationId=CorrelationId( - location="$message.header#/correlation_id", - ), ), ), bindings=ChannelBinding( amqp=amqp.ChannelBinding( - is_="routingKey", - queue=amqp.Queue( - name=self.queue.name, - durable=self.queue.durable, - exclusive=self.queue.exclusive, - autoDelete=self.queue.auto_delete, - vhost=self.virtual_host, - ) - if is_routing_exchange(self.exchange) and self.queue.name - else None, - exchange=( - amqp.Exchange(type="default", vhost=self.virtual_host) - if not self.exchange.name - else amqp.Exchange( - type=self.exchange.type.value, - name=self.exchange.name, - durable=self.exchange.durable, - autoDelete=self.exchange.auto_delete, - vhost=self.virtual_host, - ) - ), + virtual_host=self.virtual_host, + queue=queue_binding, + exchange=exchange_binding, ), ), ), diff --git a/faststream/rabbit/subscriber/usecase.py b/faststream/rabbit/subscriber/usecase.py index e91333ed25..df229a5cc4 100644 --- a/faststream/rabbit/subscriber/usecase.py +++ b/faststream/rabbit/subscriber/usecase.py @@ -15,7 +15,6 @@ from faststream.exceptions import SetupError from faststream.rabbit.parser import AioPikaParser from faststream.rabbit.publisher.fake import RabbitFakePublisher -from faststream.rabbit.schemas import BaseRMQInformation if TYPE_CHECKING: from aio_pika import IncomingMessage, RobustQueue @@ -36,10 +35,7 @@ ) -class LogicSubscriber( - SubscriberUsecase["IncomingMessage"], - BaseRMQInformation, -): +class LogicSubscriber(SubscriberUsecase["IncomingMessage"]): """A class to handle logic for RabbitMQ message consumption.""" app_id: Optional[str] @@ -53,18 +49,15 @@ def __init__( self, *, queue: "RabbitQueue", - exchange: "RabbitExchange", consume_args: Optional["AnyDict"], # Subscriber args ack_policy: "AckPolicy", no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[IncomingMessage]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: + self.queue = queue + parser = AioPikaParser(pattern=queue.path_regex) super().__init__( @@ -75,10 +68,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.consume_args = consume_args or {} @@ -86,20 +75,13 @@ def __init__( self._consumer_tag = None self._queue_obj = None - # BaseRMQInformation - self.queue = queue - self.exchange = exchange # Setup it later - self.app_id = None - self.virtual_host = "" self.declarer = None @override def _setup( # type: ignore[override] self, *, - app_id: Optional[str], - virtual_host: str, declarer: "RabbitDeclarer", # basic args extra_context: "AnyDict", @@ -109,8 +91,6 @@ def _setup( # type: ignore[override] # dependant args state: "BrokerState", ) -> None: - self.app_id = app_id - self.virtual_host = virtual_host self.declarer = declarer super()._setup( diff --git a/faststream/redis/broker/broker.py b/faststream/redis/broker/broker.py index 8b5a0a0017..bae4bd9de3 100644 --- a/faststream/redis/broker/broker.py +++ b/faststream/redis/broker/broker.py @@ -56,7 +56,7 @@ ) from faststream.redis.message import BaseMessage, RedisMessage from faststream.security import BaseSecurity - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict class RedisInitKwargs(TypedDict, total=False): host: Optional[str] @@ -162,9 +162,9 @@ def __init__( Doc("AsyncAPI server description."), ] = None, tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], diff --git a/faststream/redis/fastapi/fastapi.py b/faststream/redis/fastapi/fastapi.py index 032b04d994..3afad46036 100644 --- a/faststream/redis/fastapi/fastapi.py +++ b/faststream/redis/fastapi/fastapi.py @@ -50,7 +50,7 @@ from faststream.redis.message import UnifyRedisMessage from faststream.redis.publisher.specified import SpecificationPublisher from faststream.security import BaseSecurity - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import Tag, TagDict class RedisRouter(StreamRouter[UnifyRedisDict]): @@ -125,9 +125,9 @@ def __init__( Doc("AsyncAPI server description."), ] = None, specification_tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], diff --git a/faststream/redis/publisher/specified.py b/faststream/redis/publisher/specified.py index f0598834c6..3ccd57c931 100644 --- a/faststream/redis/publisher/specified.py +++ b/faststream/redis/publisher/specified.py @@ -1,40 +1,41 @@ from typing import TYPE_CHECKING +from faststream._internal.publisher.specified import ( + SpecificationPublisher as SpecificationPublisherMixin, +) from faststream.redis.publisher.usecase import ( ChannelPublisher, ListBatchPublisher, ListPublisher, - LogicPublisher, StreamPublisher, ) from faststream.redis.schemas.proto import RedisSpecificationProtocol from faststream.specification.asyncapi.utils import resolve_payloads +from faststream.specification.schema import Message, Operation, PublisherSpec from faststream.specification.schema.bindings import ChannelBinding, redis -from faststream.specification.schema.channel import Channel -from faststream.specification.schema.message import CorrelationId, Message -from faststream.specification.schema.operation import Operation if TYPE_CHECKING: from faststream.redis.schemas import ListSub -class SpecificationPublisher(LogicPublisher, RedisSpecificationProtocol): +class SpecificationPublisher( + SpecificationPublisherMixin, + RedisSpecificationProtocol[PublisherSpec], +): """A class to represent a Redis publisher.""" - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, PublisherSpec]: payloads = self.get_payloads() return { - self.name: Channel( + self.name: PublisherSpec( description=self.description, - publish=Operation( + operation=Operation( message=Message( title=f"{self.name}:Message", payload=resolve_payloads(payloads, "Publisher"), - correlationId=CorrelationId( - location="$message.header#/correlation_id", - ), ), + bindings=None, ), bindings=ChannelBinding( redis=self.channel_binding, @@ -43,8 +44,8 @@ def get_schema(self) -> dict[str, Channel]: } -class SpecificationChannelPublisher(ChannelPublisher, SpecificationPublisher): - def get_name(self) -> str: +class SpecificationChannelPublisher(SpecificationPublisher, ChannelPublisher): + def get_default_name(self) -> str: return f"{self.channel.name}:Publisher" @property @@ -58,7 +59,7 @@ def channel_binding(self) -> "redis.ChannelBinding": class _ListPublisherMixin(SpecificationPublisher): list: "ListSub" - def get_name(self) -> str: + def get_default_name(self) -> str: return f"{self.list.name}:Publisher" @property @@ -69,16 +70,16 @@ def channel_binding(self) -> "redis.ChannelBinding": ) -class SpecificationListPublisher(ListPublisher, _ListPublisherMixin): +class SpecificationListPublisher(_ListPublisherMixin, ListPublisher): pass -class SpecificationListBatchPublisher(ListBatchPublisher, _ListPublisherMixin): +class SpecificationListBatchPublisher(_ListPublisherMixin, ListBatchPublisher): pass -class SpecificationStreamPublisher(StreamPublisher, SpecificationPublisher): - def get_name(self) -> str: +class SpecificationStreamPublisher(SpecificationPublisher, StreamPublisher): + def get_default_name(self) -> str: return f"{self.stream.name}:Publisher" @property diff --git a/faststream/redis/publisher/usecase.py b/faststream/redis/publisher/usecase.py index 479b9ccf66..601d18130c 100644 --- a/faststream/redis/publisher/usecase.py +++ b/faststream/redis/publisher/usecase.py @@ -33,20 +33,10 @@ def __init__( # Publisher args broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], middlewares: Iterable["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( broker_middlewares=broker_middlewares, middlewares=middlewares, - # AsyncAPI args - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.reply_to = reply_to @@ -67,21 +57,12 @@ def __init__( # Regular publisher options broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], middlewares: Iterable["PublisherMiddleware"], - # AsyncAPI options - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( reply_to=reply_to, headers=headers, broker_middlewares=broker_middlewares, middlewares=middlewares, - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.channel = channel @@ -204,21 +185,12 @@ def __init__( # Regular publisher options broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], middlewares: Iterable["PublisherMiddleware"], - # AsyncAPI options - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( reply_to=reply_to, headers=headers, broker_middlewares=broker_middlewares, middlewares=middlewares, - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.list = list @@ -399,21 +371,12 @@ def __init__( # Regular publisher options broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], middlewares: Iterable["PublisherMiddleware"], - # AsyncAPI options - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( reply_to=reply_to, headers=headers, broker_middlewares=broker_middlewares, middlewares=middlewares, - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.stream = stream diff --git a/faststream/redis/schemas/proto.py b/faststream/redis/schemas/proto.py index 685d4aa679..1f4191df73 100644 --- a/faststream/redis/schemas/proto.py +++ b/faststream/redis/schemas/proto.py @@ -2,14 +2,14 @@ from typing import TYPE_CHECKING, Any, Union from faststream.exceptions import SetupError -from faststream.specification.base.proto import SpecificationEndpoint +from faststream.specification.proto.endpoint import EndpointSpecification, T if TYPE_CHECKING: from faststream.redis.schemas import ListSub, PubSub, StreamSub from faststream.specification.schema.bindings import redis -class RedisSpecificationProtocol(SpecificationEndpoint): +class RedisSpecificationProtocol(EndpointSpecification[T]): @property @abstractmethod def channel_binding(self) -> "redis.ChannelBinding": ... diff --git a/faststream/redis/subscriber/specified.py b/faststream/redis/subscriber/specified.py index 800e5b1f02..e943a80aeb 100644 --- a/faststream/redis/subscriber/specified.py +++ b/faststream/redis/subscriber/specified.py @@ -1,37 +1,37 @@ +from faststream._internal.subscriber.specified import ( + SpecificationSubscriber as SpecificationSubscriberMixin, +) from faststream.redis.schemas import ListSub, StreamSub from faststream.redis.schemas.proto import RedisSpecificationProtocol from faststream.redis.subscriber.usecase import ( BatchListSubscriber, ChannelSubscriber, ListSubscriber, - LogicSubscriber, StreamBatchSubscriber, StreamSubscriber, ) from faststream.specification.asyncapi.utils import resolve_payloads +from faststream.specification.schema import Message, Operation, SubscriberSpec from faststream.specification.schema.bindings import ChannelBinding, redis -from faststream.specification.schema.channel import Channel -from faststream.specification.schema.message import CorrelationId, Message -from faststream.specification.schema.operation import Operation -class SpecificationSubscriber(LogicSubscriber, RedisSpecificationProtocol): +class SpecificationSubscriber( + SpecificationSubscriberMixin, RedisSpecificationProtocol[SubscriberSpec] +): """A class to represent a Redis handler.""" - def get_schema(self) -> dict[str, Channel]: + def get_schema(self) -> dict[str, SubscriberSpec]: payloads = self.get_payloads() return { - self.name: Channel( + self.name: SubscriberSpec( description=self.description, - subscribe=Operation( + operation=Operation( message=Message( title=f"{self.name}:Message", payload=resolve_payloads(payloads), - correlationId=CorrelationId( - location="$message.header#/correlation_id", - ), ), + bindings=None, ), bindings=ChannelBinding( redis=self.channel_binding, @@ -40,8 +40,8 @@ def get_schema(self) -> dict[str, Channel]: } -class SpecificationChannelSubscriber(ChannelSubscriber, SpecificationSubscriber): - def get_name(self) -> str: +class SpecificationChannelSubscriber(SpecificationSubscriber, ChannelSubscriber): + def get_default_name(self) -> str: return f"{self.channel.name}:{self.call_name}" @property @@ -55,7 +55,7 @@ def channel_binding(self) -> "redis.ChannelBinding": class _StreamSubscriberMixin(SpecificationSubscriber): stream_sub: StreamSub - def get_name(self) -> str: + def get_default_name(self) -> str: return f"{self.stream_sub.name}:{self.call_name}" @property @@ -68,18 +68,18 @@ def channel_binding(self) -> "redis.ChannelBinding": ) -class SpecificationStreamSubscriber(StreamSubscriber, _StreamSubscriberMixin): +class SpecificationStreamSubscriber(_StreamSubscriberMixin, StreamSubscriber): pass -class SpecificationStreamBatchSubscriber(StreamBatchSubscriber, _StreamSubscriberMixin): +class SpecificationStreamBatchSubscriber(_StreamSubscriberMixin, StreamBatchSubscriber): pass class _ListSubscriberMixin(SpecificationSubscriber): list_sub: ListSub - def get_name(self) -> str: + def get_default_name(self) -> str: return f"{self.list_sub.name}:{self.call_name}" @property @@ -90,9 +90,9 @@ def channel_binding(self) -> "redis.ChannelBinding": ) -class SpecificationListSubscriber(ListSubscriber, _ListSubscriberMixin): +class SpecificationListSubscriber(_ListSubscriberMixin, ListSubscriber): pass -class SpecificationListBatchSubscriber(BatchListSubscriber, _ListSubscriberMixin): +class SpecificationListBatchSubscriber(_ListSubscriberMixin, BatchListSubscriber): pass diff --git a/faststream/redis/subscriber/usecase.py b/faststream/redis/subscriber/usecase.py index 1f89ca54b4..4d689a9193 100644 --- a/faststream/redis/subscriber/usecase.py +++ b/faststream/redis/subscriber/usecase.py @@ -76,10 +76,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( default_parser=default_parser, @@ -89,10 +85,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self._client = None @@ -208,10 +200,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: parser = RedisPubSubParser(pattern=channel.path_regex) super().__init__( @@ -222,10 +210,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.channel = channel @@ -333,10 +317,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( default_parser=default_parser, @@ -346,10 +326,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.list_sub = list @@ -438,10 +414,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: parser = RedisListParser() super().__init__( @@ -453,10 +425,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) async def _get_msgs(self, client: "Redis[bytes]") -> None: @@ -486,10 +454,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: parser = RedisBatchListParser() super().__init__( @@ -501,10 +465,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) async def _get_msgs(self, client: "Redis[bytes]") -> None: @@ -538,10 +498,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: super().__init__( default_parser=default_parser, @@ -551,10 +507,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) self.stream_sub = stream @@ -728,10 +680,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: parser = RedisStreamParser() super().__init__( @@ -743,10 +691,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) async def _get_msgs( @@ -795,10 +739,6 @@ def __init__( no_reply: bool, broker_dependencies: Iterable["Dependant"], broker_middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: parser = RedisBatchStreamParser() super().__init__( @@ -810,10 +750,6 @@ def __init__( no_reply=no_reply, broker_middlewares=broker_middlewares, broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) async def _get_msgs( diff --git a/faststream/specification/__init__.py b/faststream/specification/__init__.py index 502bf43de6..7738408d36 100644 --- a/faststream/specification/__init__.py +++ b/faststream/specification/__init__.py @@ -1,5 +1,5 @@ from .asyncapi.factory import AsyncAPI -from .schema import Contact, ExternalDocs, License, Tag +from .schema.extra import Contact, ExternalDocs, License, Tag __all__ = ( "AsyncAPI", diff --git a/faststream/specification/asyncapi/factory.py b/faststream/specification/asyncapi/factory.py index 7b537be3c8..5137f6cfa1 100644 --- a/faststream/specification/asyncapi/factory.py +++ b/faststream/specification/asyncapi/factory.py @@ -1,4 +1,4 @@ -from collections.abc import Sequence +from collections.abc import Iterable from typing import TYPE_CHECKING, Any, Literal, Optional, Union from faststream.specification.base.specification import Specification @@ -6,13 +6,39 @@ if TYPE_CHECKING: from faststream._internal.basic_types import AnyDict, AnyHttpUrl from faststream._internal.broker.broker import BrokerUsecase - from faststream.specification.schema.contact import Contact, ContactDict - from faststream.specification.schema.docs import ExternalDocs, ExternalDocsDict - from faststream.specification.schema.license import License, LicenseDict - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema import ( + Contact, + ContactDict, + ExternalDocs, + ExternalDocsDict, + License, + LicenseDict, + Tag, + TagDict, + ) class AsyncAPI(Specification): + # Empty init for correct typehints + def __init__( + self, + broker: "BrokerUsecase[Any, Any]", + /, + title: str = "FastStream", + app_version: str = "0.1.0", + schema_version: Union[Literal["3.0.0", "2.6.0"], str] = "3.0.0", + description: str = "", + terms_of_service: Optional["AnyHttpUrl"] = None, + license: Optional[Union["License", "LicenseDict", "AnyDict"]] = None, + contact: Optional[Union["Contact", "ContactDict", "AnyDict"]] = None, + tags: Iterable[Union["Tag", "TagDict", "AnyDict"]] = (), + external_docs: Optional[ + Union["ExternalDocs", "ExternalDocsDict", "AnyDict"] + ] = None, + identifier: Optional[str] = None, + ) -> Specification: + pass + def __new__( # type: ignore[misc] cls, broker: "BrokerUsecase[Any, Any]", @@ -24,7 +50,7 @@ def __new__( # type: ignore[misc] terms_of_service: Optional["AnyHttpUrl"] = None, license: Optional[Union["License", "LicenseDict", "AnyDict"]] = None, contact: Optional[Union["Contact", "ContactDict", "AnyDict"]] = None, - tags: Optional[Sequence[Union["Tag", "TagDict", "AnyDict"]]] = None, + tags: Iterable[Union["Tag", "TagDict", "AnyDict"]] = (), external_docs: Optional[ Union["ExternalDocs", "ExternalDocsDict", "AnyDict"] ] = None, diff --git a/faststream/specification/asyncapi/utils.py b/faststream/specification/asyncapi/utils.py index 2e6ffadfe2..7f16a215dc 100644 --- a/faststream/specification/asyncapi/utils.py +++ b/faststream/specification/asyncapi/utils.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from faststream._internal.basic_types import AnyDict @@ -49,3 +49,36 @@ def resolve_payloads( def clear_key(key: str) -> str: return key.replace("/", ".") + + +def move_pydantic_refs( + original: Any, + key: str, +) -> Any: + """Remove pydantic references and replacem them by real schemas.""" + if not isinstance(original, dict): + return original + + data = original.copy() + + for k in data: + item = data[k] + + if isinstance(item, str): + if key in item: + data[k] = data[k].replace(key, "components/schemas") + + elif isinstance(item, dict): + data[k] = move_pydantic_refs(data[k], key) + + elif isinstance(item, list): + for i in range(len(data[k])): + data[k][i] = move_pydantic_refs(item[i], key) + + if ( + isinstance(desciminator := data.get("discriminator"), dict) + and "propertyName" in desciminator + ): + data["discriminator"] = desciminator["propertyName"] + + return data diff --git a/faststream/specification/asyncapi/v2_6_0/facade.py b/faststream/specification/asyncapi/v2_6_0/facade.py index 80c7cedd38..d8c4b5618b 100644 --- a/faststream/specification/asyncapi/v2_6_0/facade.py +++ b/faststream/specification/asyncapi/v2_6_0/facade.py @@ -1,18 +1,24 @@ -from collections.abc import Sequence +from collections.abc import Iterable from typing import TYPE_CHECKING, Any, Optional, Union from faststream.specification.base.specification import Specification from .generate import get_app_schema -from .schema import Schema +from .schema import ApplicationSchema if TYPE_CHECKING: from faststream._internal.basic_types import AnyDict, AnyHttpUrl from faststream._internal.broker.broker import BrokerUsecase - from faststream.specification.schema.contact import Contact, ContactDict - from faststream.specification.schema.docs import ExternalDocs, ExternalDocsDict - from faststream.specification.schema.license import License, LicenseDict - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema import ( + Contact, + ContactDict, + ExternalDocs, + ExternalDocsDict, + License, + LicenseDict, + Tag, + TagDict, + ) class AsyncAPI2(Specification): @@ -28,7 +34,7 @@ def __init__( contact: Optional[Union["Contact", "ContactDict", "AnyDict"]] = None, license: Optional[Union["License", "LicenseDict", "AnyDict"]] = None, identifier: Optional[str] = None, - tags: Optional[Sequence[Union["Tag", "TagDict", "AnyDict"]]] = None, + tags: Iterable[Union["Tag", "TagDict", "AnyDict"]] = (), external_docs: Optional[ Union["ExternalDocs", "ExternalDocsDict", "AnyDict"] ] = None, @@ -55,7 +61,7 @@ def to_yaml(self) -> str: return self.schema.to_yaml() @property - def schema(self) -> Schema: # type: ignore[override] + def schema(self) -> ApplicationSchema: # type: ignore[override] return get_app_schema( self.broker, title=self.title, diff --git a/faststream/specification/asyncapi/v2_6_0/generate.py b/faststream/specification/asyncapi/v2_6_0/generate.py index 2c6a3b3900..4c81514da7 100644 --- a/faststream/specification/asyncapi/v2_6_0/generate.py +++ b/faststream/specification/asyncapi/v2_6_0/generate.py @@ -4,32 +4,33 @@ from faststream._internal._compat import DEF_KEY from faststream._internal.basic_types import AnyDict, AnyHttpUrl from faststream._internal.constants import ContentTypes -from faststream.specification.asyncapi.utils import clear_key +from faststream.specification.asyncapi.utils import clear_key, move_pydantic_refs from faststream.specification.asyncapi.v2_6_0.schema import ( + ApplicationInfo, + ApplicationSchema, Channel, Components, - Info, + Contact, + ExternalDocs, + License, + Message, Reference, - Schema, Server, Tag, - channel_from_spec, - contact_from_spec, - docs_from_spec, - license_from_spec, - tag_from_spec, ) -from faststream.specification.asyncapi.v2_6_0.schema.message import Message if TYPE_CHECKING: from faststream._internal.broker.broker import BrokerUsecase from faststream._internal.types import ConnectionType, MsgType - from faststream.specification.schema.contact import Contact, ContactDict - from faststream.specification.schema.docs import ExternalDocs, ExternalDocsDict - from faststream.specification.schema.license import License, LicenseDict - from faststream.specification.schema.tag import ( - Tag as SpecsTag, - TagDict as SpecsTagDict, + from faststream.specification.schema.extra import ( + Contact as SpecContact, + ContactDict, + ExternalDocs as SpecDocs, + ExternalDocsDict, + License as SpecLicense, + LicenseDict, + Tag as SpecTag, + TagDict, ) @@ -41,12 +42,12 @@ def get_app_schema( schema_version: str, description: str, terms_of_service: Optional["AnyHttpUrl"], - contact: Optional[Union["Contact", "ContactDict", "AnyDict"]], - license: Optional[Union["License", "LicenseDict", "AnyDict"]], + contact: Optional[Union["SpecContact", "ContactDict", "AnyDict"]], + license: Optional[Union["SpecLicense", "LicenseDict", "AnyDict"]], identifier: Optional[str], - tags: Optional[Sequence[Union["SpecsTag", "SpecsTagDict", "AnyDict"]]], - external_docs: Optional[Union["ExternalDocs", "ExternalDocsDict", "AnyDict"]], -) -> Schema: + tags: Sequence[Union["SpecTag", "TagDict", "AnyDict"]], + external_docs: Optional[Union["SpecDocs", "ExternalDocsDict", "AnyDict"]], +) -> ApplicationSchema: """Get the application schema.""" broker._setup() @@ -62,20 +63,20 @@ def get_app_schema( for channel_name, ch in channels.items(): resolve_channel_messages(ch, channel_name, payloads, messages) - return Schema( - info=Info( + return ApplicationSchema( + info=ApplicationInfo( title=title, version=app_version, description=description, termsOfService=terms_of_service, - contact=contact_from_spec(contact) if contact else None, - license=license_from_spec(license) if license else None, + contact=Contact.from_spec(contact), + license=License.from_spec(license), ), + tags=[Tag.from_spec(tag) for tag in tags] or None, + externalDocs=ExternalDocs.from_spec(external_docs), asyncapi=schema_version, defaultContentType=ContentTypes.JSON.value, id=identifier, - tags=[tag_from_spec(tag) for tag in tags] if tags else None, - externalDocs=docs_from_spec(external_docs) if external_docs else None, servers=servers, channels=channels, components=Components( @@ -121,31 +122,22 @@ def get_broker_server( """Get the broker server for an application.""" servers = {} - tags: Optional[list[Union[Tag, AnyDict]]] = None - if broker.tags: - tags = [tag_from_spec(tag) for tag in broker.tags] - broker_meta: AnyDict = { "protocol": broker.protocol, "protocolVersion": broker.protocol_version, "description": broker.description, - "tags": tags or None, + "tags": [Tag.from_spec(tag) for tag in broker.tags] or None, + "security": broker.security.get_requirement() if broker.security else None, # TODO # "variables": "", # "bindings": "", } - if broker.security is not None: - broker_meta["security"] = broker.security.get_requirement() - urls = broker.url if isinstance(broker.url, list) else [broker.url] for i, url in enumerate(urls, 1): server_name = "development" if len(urls) == 1 else f"Server{i}" - servers[server_name] = Server( - url=url, - **broker_meta, - ) + servers[server_name] = Server(url=url, **broker_meta) return servers @@ -157,16 +149,16 @@ def get_broker_channels( channels = {} for h in broker._subscribers: - schema = h.schema() - channels.update( - {key: channel_from_spec(channel) for key, channel in schema.items()}, - ) + # TODO: add duplication key warning + channels.update({ + key: Channel.from_sub(channel) for key, channel in h.schema().items() + }) for p in broker._publishers: - schema = p.schema() - channels.update( - {key: channel_from_spec(channel) for key, channel in schema.items()}, - ) + # TODO: add duplication key warning + channels.update({ + key: Channel.from_pub(channel) for key, channel in p.schema().items() + }) return channels @@ -223,36 +215,3 @@ def _resolve_msg_payloads( message_title = clear_key(m.title) messages[message_title] = m return Reference(**{"$ref": f"#/components/messages/{message_title}"}) - - -def move_pydantic_refs( - original: Any, - key: str, -) -> Any: - """Remove pydantic references and replacem them by real schemas.""" - if not isinstance(original, dict): - return original - - data = original.copy() - - for k in data: - item = data[k] - - if isinstance(item, str): - if key in item: - data[k] = data[k].replace(key, "components/schemas") - - elif isinstance(item, dict): - data[k] = move_pydantic_refs(data[k], key) - - elif isinstance(item, list): - for i in range(len(data[k])): - data[k][i] = move_pydantic_refs(item[i], key) - - if ( - isinstance(desciminator := data.get("discriminator"), dict) - and "propertyName" in desciminator - ): - data["discriminator"] = desciminator["propertyName"] - - return data diff --git a/faststream/specification/asyncapi/v2_6_0/schema/__init__.py b/faststream/specification/asyncapi/v2_6_0/schema/__init__.py index 1e29c5b8cd..e0cbcbd7b2 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/__init__.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/__init__.py @@ -1,61 +1,31 @@ -from .channels import ( - Channel, - from_spec as channel_from_spec, -) +from .channels import Channel from .components import Components -from .contact import ( - Contact, - from_spec as contact_from_spec, -) -from .docs import ( - ExternalDocs, - from_spec as docs_from_spec, -) -from .info import Info -from .license import ( - License, - from_spec as license_from_spec, -) -from .message import ( - CorrelationId, - Message, - from_spec as message_from_spec, -) -from .operations import ( - Operation, - from_spec as operation_from_spec, -) -from .schema import Schema +from .contact import Contact +from .docs import ExternalDocs +from .info import ApplicationInfo +from .license import License +from .message import CorrelationId, Message +from .operations import Operation +from .schema import ApplicationSchema from .servers import Server, ServerVariable -from .tag import ( - Tag, - from_spec as tag_from_spec, -) +from .tag import Tag from .utils import Parameter, Reference __all__ = ( + "ApplicationInfo", + "ApplicationSchema", "Channel", "Channel", "Components", "Contact", "CorrelationId", "ExternalDocs", - "Info", "License", "Message", "Operation", "Parameter", "Reference", - "Schema", "Server", "ServerVariable", "Tag", - "channel_from_spec", - "channel_from_spec", - "contact_from_spec", - "docs_from_spec", - "license_from_spec", - "message_from_spec", - "operation_from_spec", - "tag_from_spec", ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/__init__.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/__init__.py index a0e9cb8389..84b0fa22e8 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/__init__.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/__init__.py @@ -1,13 +1,6 @@ -from .main import ( - ChannelBinding, - OperationBinding, - channel_binding_from_spec, - operation_binding_from_spec, -) +from .main import ChannelBinding, OperationBinding __all__ = ( "ChannelBinding", "OperationBinding", - "channel_binding_from_spec", - "operation_binding_from_spec", ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/__init__.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/__init__.py index 7ead3ce532..8555fd981a 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/__init__.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/__init__.py @@ -1,15 +1,7 @@ -from .channel import ( - ChannelBinding, - from_spec as channel_binding_from_spec, -) -from .operation import ( - OperationBinding, - from_spec as operation_binding_from_spec, -) +from .channel import ChannelBinding +from .operation import OperationBinding __all__ = ( "ChannelBinding", "OperationBinding", - "channel_binding_from_spec", - "operation_binding_from_spec", ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel.py index 1317967a1d..aa729dce29 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel.py @@ -3,12 +3,12 @@ References: https://github.com/asyncapi/bindings/tree/master/amqp """ -from typing import Literal, Optional +from typing import Literal, Optional, overload from pydantic import BaseModel, Field from typing_extensions import Self -from faststream.specification import schema as spec +from faststream.specification.schema.bindings import amqp class Queue(BaseModel): @@ -28,14 +28,25 @@ class Queue(BaseModel): autoDelete: bool vhost: str = "/" + @overload @classmethod - def from_spec(cls, binding: spec.bindings.amqp.Queue) -> Self: + def from_spec(cls, binding: None, vhost: str) -> None: ... + + @overload + @classmethod + def from_spec(cls, binding: amqp.Queue, vhost: str) -> Self: ... + + @classmethod + def from_spec(cls, binding: Optional[amqp.Queue], vhost: str) -> Optional[Self]: + if binding is None: + return None + return cls( name=binding.name, durable=binding.durable, exclusive=binding.exclusive, - autoDelete=binding.autoDelete, - vhost=binding.vhost, + autoDelete=binding.auto_delete, + vhost=vhost, ) @@ -65,14 +76,25 @@ class Exchange(BaseModel): autoDelete: Optional[bool] = None vhost: str = "/" + @overload + @classmethod + def from_spec(cls, binding: None, vhost: str) -> None: ... + + @overload + @classmethod + def from_spec(cls, binding: amqp.Exchange, vhost: str) -> Self: ... + @classmethod - def from_spec(cls, binding: spec.bindings.amqp.Exchange) -> Self: + def from_spec(cls, binding: Optional[amqp.Exchange], vhost: str) -> Optional[Self]: + if binding is None: + return None + return cls( name=binding.name, type=binding.type, durable=binding.durable, - autoDelete=binding.autoDelete, - vhost=binding.vhost, + autoDelete=binding.auto_delete, + vhost=vhost, ) @@ -92,19 +114,31 @@ class ChannelBinding(BaseModel): exchange: Optional[Exchange] = None @classmethod - def from_spec(cls, binding: spec.bindings.amqp.ChannelBinding) -> Self: + def from_sub(cls, binding: Optional[amqp.ChannelBinding]) -> Optional[Self]: + if binding is None: + return None + return cls( **{ - "is": binding.is_, - "queue": Queue.from_spec(binding.queue) - if binding.queue is not None - else None, - "exchange": Exchange.from_spec(binding.exchange) - if binding.exchange is not None + "is": "routingKey", + "queue": Queue.from_spec(binding.queue, binding.virtual_host) + if binding.exchange.is_respect_routing_key else None, + "exchange": Exchange.from_spec(binding.exchange, binding.virtual_host), }, ) + @classmethod + def from_pub(cls, binding: Optional[amqp.ChannelBinding]) -> Optional[Self]: + if binding is None: + return None -def from_spec(binding: spec.bindings.amqp.ChannelBinding) -> ChannelBinding: - return ChannelBinding.from_spec(binding) + return cls( + **{ + "is": "routingKey", + "queue": Queue.from_spec(binding.queue, binding.virtual_host) + if binding.exchange.is_respect_routing_key and binding.queue.name + else None, + "exchange": Exchange.from_spec(binding.exchange, binding.virtual_host), + }, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/operation.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/operation.py index cd90dde96d..47ed19af93 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/operation.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/operation.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, PositiveInt from typing_extensions import Self -from faststream.specification import schema as spec +from faststream.specification.schema.bindings import amqp class OperationBinding(BaseModel): @@ -22,24 +22,38 @@ class OperationBinding(BaseModel): """ cc: Optional[str] = None - ack: bool = True + ack: bool replyTo: Optional[str] = None deliveryMode: Optional[int] = None mandatory: Optional[bool] = None priority: Optional[PositiveInt] = None + bindingVersion: str = "0.2.0" @classmethod - def from_spec(cls, binding: spec.bindings.amqp.OperationBinding) -> Self: + def from_sub(cls, binding: Optional[amqp.OperationBinding]) -> Optional[Self]: + if not binding: + return None + return cls( - cc=binding.cc, + cc=binding.routing_key if binding.exchange.is_respect_routing_key else None, ack=binding.ack, - replyTo=binding.replyTo, - deliveryMode=binding.deliveryMode, + replyTo=binding.reply_to, + deliveryMode=None if binding.persist is None else int(binding.persist) + 1, mandatory=binding.mandatory, priority=binding.priority, ) + @classmethod + def from_pub(cls, binding: Optional[amqp.OperationBinding]) -> Optional[Self]: + if not binding: + return None -def from_spec(binding: spec.bindings.amqp.OperationBinding) -> OperationBinding: - return OperationBinding.from_spec(binding) + return cls( + cc=binding.routing_key if binding.exchange.is_respect_routing_key else None, + ack=binding.ack, + replyTo=binding.reply_to, + deliveryMode=None if binding.persist is None else int(binding.persist) + 1, + mandatory=binding.mandatory, + priority=binding.priority, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/__init__.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/__init__.py index 7ead3ce532..8555fd981a 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/__init__.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/__init__.py @@ -1,15 +1,7 @@ -from .channel import ( - ChannelBinding, - from_spec as channel_binding_from_spec, -) -from .operation import ( - OperationBinding, - from_spec as operation_binding_from_spec, -) +from .channel import ChannelBinding +from .operation import OperationBinding __all__ = ( "ChannelBinding", "OperationBinding", - "channel_binding_from_spec", - "operation_binding_from_spec", ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/channel.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/channel.py index d6eda36274..1f304410ba 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/channel.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/channel.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, PositiveInt from typing_extensions import Self -from faststream.specification import schema as spec +from faststream.specification.schema.bindings import kafka class ChannelBinding(BaseModel): @@ -30,14 +30,23 @@ class ChannelBinding(BaseModel): # topicConfiguration @classmethod - def from_spec(cls, binding: spec.bindings.kafka.ChannelBinding) -> Self: + def from_sub(cls, binding: Optional[kafka.ChannelBinding]) -> Optional[Self]: + if binding is None: + return None + return cls( topic=binding.topic, partitions=binding.partitions, replicas=binding.replicas, - bindingVersion=binding.bindingVersion, ) + @classmethod + def from_pub(cls, binding: Optional[kafka.ChannelBinding]) -> Optional[Self]: + if binding is None: + return None -def from_spec(binding: spec.bindings.kafka.ChannelBinding) -> ChannelBinding: - return ChannelBinding.from_spec(binding) + return cls( + topic=binding.topic, + partitions=binding.partitions, + replicas=binding.replicas, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/operation.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/operation.py index 005cc92cf7..4155ce220e 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/operation.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/operation.py @@ -9,7 +9,7 @@ from typing_extensions import Self from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec +from faststream.specification.schema.bindings import kafka class OperationBinding(BaseModel): @@ -28,14 +28,23 @@ class OperationBinding(BaseModel): bindingVersion: str = "0.4.0" @classmethod - def from_spec(cls, binding: spec.bindings.kafka.OperationBinding) -> Self: + def from_sub(cls, binding: Optional[kafka.OperationBinding]) -> Optional[Self]: + if not binding: + return None + return cls( - groupId=binding.groupId, - clientId=binding.clientId, - replyTo=binding.replyTo, - bindingVersion=binding.bindingVersion, + groupId=binding.group_id, + clientId=binding.client_id, + replyTo=binding.reply_to, ) + @classmethod + def from_pub(cls, binding: Optional[kafka.OperationBinding]) -> Optional[Self]: + if not binding: + return None -def from_spec(binding: spec.bindings.kafka.OperationBinding) -> OperationBinding: - return OperationBinding.from_spec(binding) + return cls( + groupId=binding.group_id, + clientId=binding.client_id, + replyTo=binding.reply_to, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/__init__.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/__init__.py index 7ead3ce532..8555fd981a 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/__init__.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/__init__.py @@ -1,15 +1,7 @@ -from .channel import ( - ChannelBinding, - from_spec as channel_binding_from_spec, -) -from .operation import ( - OperationBinding, - from_spec as operation_binding_from_spec, -) +from .channel import ChannelBinding +from .operation import OperationBinding __all__ = ( "ChannelBinding", "OperationBinding", - "channel_binding_from_spec", - "operation_binding_from_spec", ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/channel.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/channel.py index 258e08ea3a..bf4b7dbd98 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/channel.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/channel.py @@ -1,10 +1,9 @@ -from typing import Optional +from typing import Optional, overload from pydantic import BaseModel from typing_extensions import Self from faststream._internal._compat import PYDANTIC_V2 -from faststream.specification import schema as spec from faststream.specification.asyncapi.v2_6_0.schema.bindings import ( amqp as amqp_bindings, kafka as kafka_bindings, @@ -12,6 +11,7 @@ redis as redis_bindings, sqs as sqs_bindings, ) +from faststream.specification.schema.bindings import ChannelBinding as SpecBinding class ChannelBinding(BaseModel): @@ -23,7 +23,6 @@ class ChannelBinding(BaseModel): sqs : SQS channel binding (optional) nats : NATS channel binding (optional) redis : Redis channel binding (optional) - """ amqp: Optional[amqp_bindings.ChannelBinding] = None @@ -40,26 +39,78 @@ class ChannelBinding(BaseModel): class Config: extra = "allow" + @overload + @classmethod + def from_sub(cls, binding: None) -> None: ... + + @overload @classmethod - def from_spec(cls, binding: spec.bindings.ChannelBinding) -> Self: - return cls( - amqp=amqp_bindings.channel_binding_from_spec(binding.amqp) - if binding.amqp is not None - else None, - kafka=kafka_bindings.channel_binding_from_spec(binding.kafka) - if binding.kafka is not None - else None, - sqs=sqs_bindings.channel_binding_from_spec(binding.sqs) - if binding.sqs is not None - else None, - nats=nats_bindings.channel_binding_from_spec(binding.nats) - if binding.nats is not None - else None, - redis=redis_bindings.channel_binding_from_spec(binding.redis) - if binding.redis is not None - else None, - ) - - -def from_spec(binding: spec.bindings.ChannelBinding) -> ChannelBinding: - return ChannelBinding.from_spec(binding) + def from_sub(cls, binding: SpecBinding) -> Self: ... + + @classmethod + def from_sub(cls, binding: Optional[SpecBinding]) -> Optional[Self]: + if binding is None: + return None + + if binding.amqp and ( + amqp := amqp_bindings.ChannelBinding.from_sub(binding.amqp) + ): + return cls(amqp=amqp) + + if binding.kafka and ( + kafka := kafka_bindings.ChannelBinding.from_sub(binding.kafka) + ): + return cls(kafka=kafka) + + if binding.nats and ( + nats := nats_bindings.ChannelBinding.from_sub(binding.nats) + ): + return cls(nats=nats) + + if binding.redis and ( + redis := redis_bindings.ChannelBinding.from_sub(binding.redis) + ): + return cls(redis=redis) + + if binding.sqs and (sqs := sqs_bindings.ChannelBinding.from_sub(binding.sqs)): + return cls(sqs=sqs) + + return None + + @overload + @classmethod + def from_pub(cls, binding: None) -> None: ... + + @overload + @classmethod + def from_pub(cls, binding: SpecBinding) -> Self: ... + + @classmethod + def from_pub(cls, binding: Optional[SpecBinding]) -> Optional[Self]: + if binding is None: + return None + + if binding.amqp and ( + amqp := amqp_bindings.ChannelBinding.from_pub(binding.amqp) + ): + return cls(amqp=amqp) + + if binding.kafka and ( + kafka := kafka_bindings.ChannelBinding.from_pub(binding.kafka) + ): + return cls(kafka=kafka) + + if binding.nats and ( + nats := nats_bindings.ChannelBinding.from_pub(binding.nats) + ): + return cls(nats=nats) + + if binding.redis and ( + redis := redis_bindings.ChannelBinding.from_pub(binding.redis) + ): + return cls(redis=redis) + + if binding.sqs and (sqs := sqs_bindings.ChannelBinding.from_pub(binding.sqs)): + return cls(sqs=sqs) + + return None diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/operation.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/operation.py index 61d614dd4d..7367b7921f 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/operation.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/operation.py @@ -1,10 +1,9 @@ -from typing import Optional +from typing import Optional, overload from pydantic import BaseModel from typing_extensions import Self from faststream._internal._compat import PYDANTIC_V2 -from faststream.specification import schema as spec from faststream.specification.asyncapi.v2_6_0.schema.bindings import ( amqp as amqp_bindings, kafka as kafka_bindings, @@ -12,6 +11,7 @@ redis as redis_bindings, sqs as sqs_bindings, ) +from faststream.specification.schema.bindings import OperationBinding as SpecBinding class OperationBinding(BaseModel): @@ -23,7 +23,6 @@ class OperationBinding(BaseModel): sqs : SQS operation binding (optional) nats : NATS operation binding (optional) redis : Redis operation binding (optional) - """ amqp: Optional[amqp_bindings.OperationBinding] = None @@ -40,26 +39,78 @@ class OperationBinding(BaseModel): class Config: extra = "allow" + @overload + @classmethod + def from_sub(cls, binding: None) -> None: ... + + @overload @classmethod - def from_spec(cls, binding: spec.bindings.OperationBinding) -> Self: - return cls( - amqp=amqp_bindings.operation_binding_from_spec(binding.amqp) - if binding.amqp is not None - else None, - kafka=kafka_bindings.operation_binding_from_spec(binding.kafka) - if binding.kafka is not None - else None, - sqs=sqs_bindings.operation_binding_from_spec(binding.sqs) - if binding.sqs is not None - else None, - nats=nats_bindings.operation_binding_from_spec(binding.nats) - if binding.nats is not None - else None, - redis=redis_bindings.operation_binding_from_spec(binding.redis) - if binding.redis is not None - else None, - ) - - -def from_spec(binding: spec.bindings.OperationBinding) -> OperationBinding: - return OperationBinding.from_spec(binding) + def from_sub(cls, binding: SpecBinding) -> Self: ... + + @classmethod + def from_sub(cls, binding: Optional[SpecBinding]) -> Optional[Self]: + if binding is None: + return None + + if binding.amqp and ( + amqp := amqp_bindings.OperationBinding.from_sub(binding.amqp) + ): + return cls(amqp=amqp) + + if binding.kafka and ( + kafka := kafka_bindings.OperationBinding.from_sub(binding.kafka) + ): + return cls(kafka=kafka) + + if binding.nats and ( + nats := nats_bindings.OperationBinding.from_sub(binding.nats) + ): + return cls(nats=nats) + + if binding.redis and ( + redis := redis_bindings.OperationBinding.from_sub(binding.redis) + ): + return cls(redis=redis) + + if binding.sqs and (sqs := sqs_bindings.OperationBinding.from_sub(binding.sqs)): + return cls(sqs=sqs) + + return None + + @overload + @classmethod + def from_pub(cls, binding: None) -> None: ... + + @overload + @classmethod + def from_pub(cls, binding: SpecBinding) -> Self: ... + + @classmethod + def from_pub(cls, binding: Optional[SpecBinding]) -> Optional[Self]: + if binding is None: + return None + + if binding.amqp and ( + amqp := amqp_bindings.OperationBinding.from_pub(binding.amqp) + ): + return cls(amqp=amqp) + + if binding.kafka and ( + kafka := kafka_bindings.OperationBinding.from_pub(binding.kafka) + ): + return cls(kafka=kafka) + + if binding.nats and ( + nats := nats_bindings.OperationBinding.from_pub(binding.nats) + ): + return cls(nats=nats) + + if binding.redis and ( + redis := redis_bindings.OperationBinding.from_pub(binding.redis) + ): + return cls(redis=redis) + + if binding.sqs and (sqs := sqs_bindings.OperationBinding.from_pub(binding.sqs)): + return cls(sqs=sqs) + + return None diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/__init__.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/__init__.py index 7ead3ce532..8555fd981a 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/__init__.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/__init__.py @@ -1,15 +1,7 @@ -from .channel import ( - ChannelBinding, - from_spec as channel_binding_from_spec, -) -from .operation import ( - OperationBinding, - from_spec as operation_binding_from_spec, -) +from .channel import ChannelBinding +from .operation import OperationBinding __all__ = ( "ChannelBinding", "OperationBinding", - "channel_binding_from_spec", - "operation_binding_from_spec", ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/channel.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/channel.py index ba39c7569b..4cc83faddb 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/channel.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/channel.py @@ -8,7 +8,7 @@ from pydantic import BaseModel from typing_extensions import Self -from faststream.specification import schema as spec +from faststream.specification.schema.bindings import nats class ChannelBinding(BaseModel): @@ -25,13 +25,23 @@ class ChannelBinding(BaseModel): bindingVersion: str = "custom" @classmethod - def from_spec(cls, binding: spec.bindings.nats.ChannelBinding) -> Self: + def from_sub(cls, binding: Optional[nats.ChannelBinding]) -> Optional[Self]: + if binding is None: + return None + return cls( subject=binding.subject, queue=binding.queue, - bindingVersion=binding.bindingVersion, + bindingVersion="custom", ) + @classmethod + def from_pub(cls, binding: Optional[nats.ChannelBinding]) -> Optional[Self]: + if binding is None: + return None -def from_spec(binding: spec.bindings.nats.ChannelBinding) -> ChannelBinding: - return ChannelBinding.from_spec(binding) + return cls( + subject=binding.subject, + queue=binding.queue, + bindingVersion="custom", + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/operation.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/operation.py index b38a2f89dd..5e1514fcba 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/operation.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/operation.py @@ -9,7 +9,7 @@ from typing_extensions import Self from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec +from faststream.specification.schema.bindings import nats class OperationBinding(BaseModel): @@ -24,12 +24,19 @@ class OperationBinding(BaseModel): bindingVersion: str = "custom" @classmethod - def from_spec(cls, binding: spec.bindings.nats.OperationBinding) -> Self: + def from_sub(cls, binding: Optional[nats.OperationBinding]) -> Optional[Self]: + if not binding: + return None + return cls( - replyTo=binding.replyTo, - bindingVersion=binding.bindingVersion, + replyTo=binding.reply_to, ) + @classmethod + def from_pub(cls, binding: Optional[nats.OperationBinding]) -> Optional[Self]: + if not binding: + return None -def from_spec(binding: spec.bindings.nats.OperationBinding) -> OperationBinding: - return OperationBinding.from_spec(binding) + return cls( + replyTo=binding.reply_to, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/__init__.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/__init__.py index 7ead3ce532..8555fd981a 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/__init__.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/__init__.py @@ -1,15 +1,7 @@ -from .channel import ( - ChannelBinding, - from_spec as channel_binding_from_spec, -) -from .operation import ( - OperationBinding, - from_spec as operation_binding_from_spec, -) +from .channel import ChannelBinding +from .operation import OperationBinding __all__ = ( "ChannelBinding", "OperationBinding", - "channel_binding_from_spec", - "operation_binding_from_spec", ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/channel.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/channel.py index 579f9170ea..abc5bf96d6 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/channel.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/channel.py @@ -8,7 +8,7 @@ from pydantic import BaseModel from typing_extensions import Self -from faststream.specification import schema as spec +from faststream.specification.schema.bindings import redis class ChannelBinding(BaseModel): @@ -22,20 +22,30 @@ class ChannelBinding(BaseModel): channel: str method: Optional[str] = None - group_name: Optional[str] = None - consumer_name: Optional[str] = None + groupName: Optional[str] = None + consumerName: Optional[str] = None bindingVersion: str = "custom" @classmethod - def from_spec(cls, binding: spec.bindings.redis.ChannelBinding) -> Self: + def from_sub(cls, binding: Optional[redis.ChannelBinding]) -> Optional[Self]: + if binding is None: + return None + return cls( channel=binding.channel, method=binding.method, - group_name=binding.group_name, - consumer_name=binding.consumer_name, - bindingVersion=binding.bindingVersion, + groupName=binding.group_name, + consumerName=binding.consumer_name, ) + @classmethod + def from_pub(cls, binding: Optional[redis.ChannelBinding]) -> Optional[Self]: + if binding is None: + return None -def from_spec(binding: spec.bindings.redis.ChannelBinding) -> ChannelBinding: - return ChannelBinding.from_spec(binding) + return cls( + channel=binding.channel, + method=binding.method, + groupName=binding.group_name, + consumerName=binding.consumer_name, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/operation.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/operation.py index 39a4c94b99..cce0316160 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/operation.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/operation.py @@ -9,7 +9,7 @@ from typing_extensions import Self from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec +from faststream.specification.schema.bindings import redis class OperationBinding(BaseModel): @@ -24,12 +24,19 @@ class OperationBinding(BaseModel): bindingVersion: str = "custom" @classmethod - def from_spec(cls, binding: spec.bindings.redis.OperationBinding) -> Self: + def from_sub(cls, binding: Optional[redis.OperationBinding]) -> Optional[Self]: + if not binding: + return None + return cls( - replyTo=binding.replyTo, - bindingVersion=binding.bindingVersion, + replyTo=binding.reply_to, ) + @classmethod + def from_pub(cls, binding: Optional[redis.OperationBinding]) -> Optional[Self]: + if not binding: + return None -def from_spec(binding: spec.bindings.redis.OperationBinding) -> OperationBinding: - return OperationBinding.from_spec(binding) + return cls( + replyTo=binding.reply_to, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/__init__.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/__init__.py index 7ead3ce532..33cdca3a8b 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/__init__.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/__init__.py @@ -1,15 +1,4 @@ -from .channel import ( - ChannelBinding, - from_spec as channel_binding_from_spec, -) -from .operation import ( - OperationBinding, - from_spec as operation_binding_from_spec, -) +from .channel import ChannelBinding +from .operation import OperationBinding -__all__ = ( - "ChannelBinding", - "OperationBinding", - "channel_binding_from_spec", - "operation_binding_from_spec", -) +__all__ = ("ChannelBinding", "OperationBinding") diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/channel.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/channel.py index 631e9c7bb4..93a1c5ac80 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/channel.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/channel.py @@ -7,7 +7,7 @@ from typing_extensions import Self from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec +from faststream.specification.schema.bindings import sqs class ChannelBinding(BaseModel): @@ -22,12 +22,12 @@ class ChannelBinding(BaseModel): bindingVersion: str = "custom" @classmethod - def from_spec(cls, binding: spec.bindings.sqs.ChannelBinding) -> Self: + def from_spec(cls, binding: sqs.ChannelBinding) -> Self: return cls( queue=binding.queue, bindingVersion=binding.bindingVersion, ) -def from_spec(binding: spec.bindings.sqs.ChannelBinding) -> ChannelBinding: +def from_spec(binding: sqs.ChannelBinding) -> ChannelBinding: return ChannelBinding.from_spec(binding) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/operation.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/operation.py index 4ea0ece20a..35aa598d20 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/operation.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/operation.py @@ -9,7 +9,7 @@ from typing_extensions import Self from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec +from faststream.specification.schema.bindings import sqs class OperationBinding(BaseModel): @@ -24,12 +24,12 @@ class OperationBinding(BaseModel): bindingVersion: str = "custom" @classmethod - def from_spec(cls, binding: spec.bindings.sqs.OperationBinding) -> Self: + def from_spec(cls, binding: sqs.OperationBinding) -> Self: return cls( replyTo=binding.replyTo, bindingVersion=binding.bindingVersion, ) -def from_spec(binding: spec.bindings.sqs.OperationBinding) -> OperationBinding: +def from_spec(binding: sqs.OperationBinding) -> OperationBinding: return OperationBinding.from_spec(binding) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/channels.py b/faststream/specification/asyncapi/v2_6_0/schema/channels.py index 99ac5585bc..936248dfd5 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/channels.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/channels.py @@ -4,15 +4,10 @@ from typing_extensions import Self from faststream._internal._compat import PYDANTIC_V2 -from faststream.specification import schema as spec -from faststream.specification.asyncapi.v2_6_0.schema.bindings import ( - ChannelBinding, - channel_binding_from_spec, -) -from faststream.specification.asyncapi.v2_6_0.schema.operations import ( - Operation, - from_spec as operation_from_spec, -) +from faststream.specification.schema import PublisherSpec, SubscriberSpec + +from .bindings import ChannelBinding +from .operations import Operation class Channel(BaseModel): @@ -28,7 +23,6 @@ class Channel(BaseModel): Configurations: model_config : configuration for the model (only applicable for Pydantic version 2) Config : configuration for the class (only applicable for Pydantic version 1) - """ description: Optional[str] = None @@ -49,21 +43,21 @@ class Config: extra = "allow" @classmethod - def from_spec(cls, channel: spec.channel.Channel) -> Self: + def from_sub(cls, subscriber: SubscriberSpec) -> Self: return cls( - description=channel.description, - servers=channel.servers, - bindings=channel_binding_from_spec(channel.bindings) - if channel.bindings is not None - else None, - subscribe=operation_from_spec(channel.subscribe) - if channel.subscribe is not None - else None, - publish=operation_from_spec(channel.publish) - if channel.publish is not None - else None, + description=subscriber.description, + servers=None, + bindings=ChannelBinding.from_sub(subscriber.bindings), + subscribe=Operation.from_sub(subscriber.operation), + publish=None, ) - -def from_spec(channel: spec.channel.Channel) -> Channel: - return Channel.from_spec(channel) + @classmethod + def from_pub(cls, publisher: PublisherSpec) -> Self: + return cls( + description=publisher.description, + servers=None, + bindings=ChannelBinding.from_pub(publisher.bindings), + subscribe=None, + publish=Operation.from_pub(publisher.operation), + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/components.py b/faststream/specification/asyncapi/v2_6_0/schema/components.py index 764d639a24..a80c3420d0 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/components.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/components.py @@ -36,7 +36,6 @@ class Components(BaseModel): - channelBindings - operationBindings - messageBindings - """ messages: Optional[dict[str, Message]] = None diff --git a/faststream/specification/asyncapi/v2_6_0/schema/contact.py b/faststream/specification/asyncapi/v2_6_0/schema/contact.py index 809b5190c4..9456d7dd61 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/contact.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/contact.py @@ -1,15 +1,15 @@ -from typing import ( - Optional, - Union, - overload, -) +from typing import Optional, Union, overload from pydantic import AnyHttpUrl, BaseModel from typing_extensions import Self from faststream._internal._compat import PYDANTIC_V2, EmailStr from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec +from faststream._internal.utils.data import filter_by_dict +from faststream.specification.schema.extra import ( + Contact as SpecContact, + ContactDict, +) class Contact(BaseModel): @@ -19,10 +19,10 @@ class Contact(BaseModel): name : name of the contact (str) url : URL of the contact (Optional[AnyHttpUrl]) email : email of the contact (Optional[EmailStr]) - """ name: str + # Use default values to be able build from dict url: Optional[AnyHttpUrl] = None email: Optional[EmailStr] = None @@ -34,31 +34,39 @@ class Contact(BaseModel): class Config: extra = "allow" + @overload @classmethod - def from_spec(cls, contact: spec.contact.Contact) -> Self: - return cls( - name=contact.name, - url=contact.url, - email=contact.email, - ) - + def from_spec(cls, contact: None) -> None: ... -@overload -def from_spec(contact: spec.contact.Contact) -> Contact: ... + @overload + @classmethod + def from_spec(cls, contact: SpecContact) -> Self: ... + @overload + @classmethod + def from_spec(cls, contact: ContactDict) -> Self: ... -@overload -def from_spec(contact: spec.contact.ContactDict) -> AnyDict: ... + @overload + @classmethod + def from_spec(cls, contact: AnyDict) -> AnyDict: ... + @classmethod + def from_spec( + cls, contact: Union[SpecContact, ContactDict, AnyDict, None] + ) -> Union[Self, AnyDict, None]: + if contact is None: + return None -@overload -def from_spec(contact: AnyDict) -> AnyDict: ... + if isinstance(contact, SpecContact): + return cls( + name=contact.name, + url=contact.url, + email=contact.email, + ) + contact_data, custom_data = filter_by_dict(ContactDict, contact) -def from_spec( - contact: Union[spec.contact.Contact, spec.contact.ContactDict, AnyDict], -) -> Union[Contact, AnyDict]: - if isinstance(contact, spec.contact.Contact): - return Contact.from_spec(contact) + if custom_data: + return contact - return dict(contact) + return cls(**contact_data) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/docs.py b/faststream/specification/asyncapi/v2_6_0/schema/docs.py index 34b2e3ed8d..6ad8d6a252 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/docs.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/docs.py @@ -5,7 +5,11 @@ from faststream._internal._compat import PYDANTIC_V2 from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec +from faststream._internal.utils.data import filter_by_dict +from faststream.specification.schema.extra import ( + ExternalDocs as SpecDocs, + ExternalDocsDict, +) class ExternalDocs(BaseModel): @@ -14,10 +18,10 @@ class ExternalDocs(BaseModel): Attributes: url : URL of the external documentation description : optional description of the external documentation - """ url: AnyHttpUrl + # Use default values to be able build from dict description: Optional[str] = None if PYDANTIC_V2: @@ -28,27 +32,35 @@ class ExternalDocs(BaseModel): class Config: extra = "allow" + @overload @classmethod - def from_spec(cls, docs: spec.docs.ExternalDocs) -> Self: - return cls(url=docs.url, description=docs.description) - + def from_spec(cls, docs: None) -> None: ... -@overload -def from_spec(docs: spec.docs.ExternalDocs) -> ExternalDocs: ... + @overload + @classmethod + def from_spec(cls, docs: SpecDocs) -> Self: ... + @overload + @classmethod + def from_spec(cls, docs: ExternalDocsDict) -> Self: ... -@overload -def from_spec(docs: spec.docs.ExternalDocsDict) -> AnyDict: ... + @overload + @classmethod + def from_spec(cls, docs: AnyDict) -> AnyDict: ... + @classmethod + def from_spec( + cls, docs: Union[SpecDocs, ExternalDocsDict, AnyDict, None] + ) -> Union[Self, AnyDict, None]: + if docs is None: + return None -@overload -def from_spec(docs: AnyDict) -> AnyDict: ... + if isinstance(docs, SpecDocs): + return cls(url=docs.url, description=docs.description) + docs_data, custom_data = filter_by_dict(ExternalDocsDict, docs) -def from_spec( - docs: Union[spec.docs.ExternalDocs, spec.docs.ExternalDocsDict, AnyDict], -) -> Union[ExternalDocs, AnyDict]: - if isinstance(docs, spec.docs.ExternalDocs): - return ExternalDocs.from_spec(docs) + if custom_data: + return docs - return dict(docs) + return cls(**docs_data) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/info.py b/faststream/specification/asyncapi/v2_6_0/schema/info.py index b4cf3bceec..50f79fa026 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/info.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/info.py @@ -8,19 +8,19 @@ from faststream._internal.basic_types import AnyDict from faststream.specification.asyncapi.v2_6_0.schema.contact import Contact from faststream.specification.asyncapi.v2_6_0.schema.license import License -from faststream.specification.base.info import BaseInfo +from faststream.specification.base.info import BaseApplicationInfo -class Info(BaseInfo): - """A class to represent information. +class ApplicationInfo(BaseApplicationInfo): + """A class to represent application information. Attributes: title : title of the information - version : version of the information (default: "1.0.0") - description : description of the information (default: "") - termsOfService : terms of service for the information (default: None) - contact : contact information for the information (default: None) - license : license information for the information (default: None) + version : version of the information + description : description of the information + termsOfService : terms of service for the information + contact : contact information for the information + license : license information for the information """ termsOfService: Optional[AnyHttpUrl] = None diff --git a/faststream/specification/asyncapi/v2_6_0/schema/license.py b/faststream/specification/asyncapi/v2_6_0/schema/license.py index 761511789a..1d3b778d8e 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/license.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/license.py @@ -1,17 +1,15 @@ -from typing import ( - Optional, - Union, - overload, -) +from typing import Optional, Union, overload from pydantic import AnyHttpUrl, BaseModel from typing_extensions import Self -from faststream._internal._compat import ( - PYDANTIC_V2, -) +from faststream._internal._compat import PYDANTIC_V2 from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec +from faststream._internal.utils.data import filter_by_dict +from faststream.specification.schema.extra import ( + License as SpecLicense, + LicenseDict, +) class License(BaseModel): @@ -23,10 +21,10 @@ class License(BaseModel): Config: extra : allow additional attributes in the model (PYDANTIC_V2) - """ name: str + # Use default values to be able build from dict url: Optional[AnyHttpUrl] = None if PYDANTIC_V2: @@ -37,30 +35,38 @@ class License(BaseModel): class Config: extra = "allow" + @overload @classmethod - def from_spec(cls, license: spec.license.License) -> Self: - return cls( - name=license.name, - url=license.url, - ) - + def from_spec(cls, license: None) -> None: ... -@overload -def from_spec(license: spec.license.License) -> License: ... + @overload + @classmethod + def from_spec(cls, license: SpecLicense) -> Self: ... + @overload + @classmethod + def from_spec(cls, license: LicenseDict) -> Self: ... -@overload -def from_spec(license: spec.license.LicenseDict) -> AnyDict: ... + @overload + @classmethod + def from_spec(cls, license: AnyDict) -> AnyDict: ... + @classmethod + def from_spec( + cls, license: Union[SpecLicense, LicenseDict, AnyDict, None] + ) -> Union[Self, AnyDict, None]: + if license is None: + return None -@overload -def from_spec(license: AnyDict) -> AnyDict: ... + if isinstance(license, SpecLicense): + return cls( + name=license.name, + url=license.url, + ) + license_data, custom_data = filter_by_dict(LicenseDict, license) -def from_spec( - license: Union[spec.license.License, spec.license.LicenseDict, AnyDict], -) -> Union[License, AnyDict]: - if isinstance(license, spec.license.License): - return License.from_spec(license) + if custom_data: + return license - return dict(license) + return cls(**license_data) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/message.py b/faststream/specification/asyncapi/v2_6_0/schema/message.py index d9cde0e403..5f56df156c 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/message.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/message.py @@ -1,15 +1,12 @@ from typing import Optional, Union -import typing_extensions from pydantic import BaseModel +from typing_extensions import Self from faststream._internal._compat import PYDANTIC_V2 from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec -from faststream.specification.asyncapi.v2_6_0.schema.tag import ( - Tag, - from_spec as tag_from_spec, -) +from faststream.specification.asyncapi.v2_6_0.schema.tag import Tag +from faststream.specification.schema.message import Message as SpecMessage class CorrelationId(BaseModel): @@ -21,11 +18,10 @@ class CorrelationId(BaseModel): Configurations: extra : allows extra fields in the correlation ID model - """ - description: Optional[str] = None location: str + description: Optional[str] = None if PYDANTIC_V2: model_config = {"extra": "allow"} @@ -35,13 +31,6 @@ class CorrelationId(BaseModel): class Config: extra = "allow" - @classmethod - def from_spec(cls, cor_id: spec.message.CorrelationId) -> typing_extensions.Self: - return cls( - description=cor_id.description, - location=cor_id.location, - ) - class Message(BaseModel): """A class to represent a message. @@ -56,7 +45,6 @@ class Message(BaseModel): contentType : content type of the message payload : dictionary representing the payload of the message tags : list of tags associated with the message - """ title: Optional[str] = None @@ -86,23 +74,18 @@ class Config: extra = "allow" @classmethod - def from_spec(cls, message: spec.message.Message) -> typing_extensions.Self: + def from_spec(cls, message: SpecMessage) -> Self: return cls( title=message.title, - name=message.name, - summary=message.summary, - description=message.description, - messageId=message.messageId, - correlationId=CorrelationId.from_spec(message.correlationId) - if message.correlationId is not None - else None, - contentType=message.contentType, payload=message.payload, - tags=[tag_from_spec(tag) for tag in message.tags] - if message.tags is not None - else None, + correlationId=CorrelationId( + description=None, + location="$message.header#/correlation_id", + ), + name=None, + summary=None, + description=None, + messageId=None, + contentType=None, + tags=None, ) - - -def from_spec(message: spec.message.Message) -> Message: - return Message.from_spec(message) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/operations.py b/faststream/specification/asyncapi/v2_6_0/schema/operations.py index 9d6c6d61fa..c837c844d7 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/operations.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/operations.py @@ -5,22 +5,12 @@ from faststream._internal._compat import PYDANTIC_V2 from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec -from faststream.specification.asyncapi.v2_6_0.schema.bindings import OperationBinding -from faststream.specification.asyncapi.v2_6_0.schema.bindings.main import ( - operation_binding_from_spec, -) -from faststream.specification.asyncapi.v2_6_0.schema.message import ( - Message, - from_spec as message_from_spec, -) -from faststream.specification.asyncapi.v2_6_0.schema.tag import ( - Tag, - from_spec as tag_from_spec, -) -from faststream.specification.asyncapi.v2_6_0.schema.utils import ( - Reference, -) +from faststream.specification.schema.operation import Operation as OperationSpec + +from .bindings import OperationBinding +from .message import Message +from .tag import Tag +from .utils import Reference class Operation(BaseModel): @@ -34,7 +24,6 @@ class Operation(BaseModel): message : message of the operation security : security details of the operation tags : tags associated with the operation - """ operationId: Optional[str] = None @@ -61,22 +50,25 @@ class Config: extra = "allow" @classmethod - def from_spec(cls, operation: spec.operation.Operation) -> Self: + def from_sub(cls, operation: OperationSpec) -> Self: return cls( - operationId=operation.operationId, - summary=operation.summary, - description=operation.description, - bindings=operation_binding_from_spec(operation.bindings) - if operation.bindings is not None - else None, - message=message_from_spec(operation.message) - if operation.message is not None - else None, - tags=[tag_from_spec(tag) for tag in operation.tags] - if operation.tags is not None - else None, + message=Message.from_spec(operation.message), + bindings=OperationBinding.from_sub(operation.bindings), + operationId=None, + summary=None, + description=None, + tags=None, + security=None, ) - -def from_spec(operation: spec.operation.Operation) -> Operation: - return Operation.from_spec(operation) + @classmethod + def from_pub(cls, operation: OperationSpec) -> Self: + return cls( + message=Message.from_spec(operation.message), + bindings=OperationBinding.from_pub(operation.bindings), + operationId=None, + summary=None, + description=None, + tags=None, + security=None, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/schema.py b/faststream/specification/asyncapi/v2_6_0/schema/schema.py index 9c19130cc2..8f4a70a701 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/schema.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/schema.py @@ -4,14 +4,14 @@ from faststream.specification.asyncapi.v2_6_0.schema.channels import Channel from faststream.specification.asyncapi.v2_6_0.schema.components import Components from faststream.specification.asyncapi.v2_6_0.schema.docs import ExternalDocs -from faststream.specification.asyncapi.v2_6_0.schema.info import Info +from faststream.specification.asyncapi.v2_6_0.schema.info import ApplicationInfo from faststream.specification.asyncapi.v2_6_0.schema.servers import Server from faststream.specification.asyncapi.v2_6_0.schema.tag import Tag -from faststream.specification.base.schema import BaseSchema +from faststream.specification.base.schema import BaseApplicationSchema -class Schema(BaseSchema): - """A class to represent a schema. +class ApplicationSchema(BaseApplicationSchema): + """A class to represent an application schema. Attributes: asyncapi : version of the async API @@ -25,9 +25,9 @@ class Schema(BaseSchema): externalDocs : optional external documentation """ - info: Info + info: ApplicationInfo - asyncapi: Union[Literal["2.6.0"], str] = "2.6.0" + asyncapi: Union[Literal["2.6.0"], str] id: Optional[str] = None defaultContentType: Optional[str] = None servers: Optional[dict[str, Server]] = None diff --git a/faststream/specification/asyncapi/v2_6_0/schema/servers.py b/faststream/specification/asyncapi/v2_6_0/schema/servers.py index a50be2669e..cae721cfd1 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/servers.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/servers.py @@ -18,7 +18,6 @@ class ServerVariable(BaseModel): default : default value for the server variable (optional) description : description of the server variable (optional) examples : list of example values for the server variable (optional) - """ enum: Optional[list[str]] = None @@ -50,19 +49,15 @@ class Server(BaseModel): Note: The attributes `description`, `protocolVersion`, `tags`, `security`, `variables`, and `bindings` are all optional. - - Configurations: - If `PYDANTIC_V2` is True, the model configuration is set to allow extra attributes. - Otherwise, the `Config` class is defined with the `extra` attribute set to "allow". - """ url: str protocol: str - description: Optional[str] = None - protocolVersion: Optional[str] = None - tags: Optional[list[Union[Tag, AnyDict]]] = None - security: Optional[SecurityRequirement] = None + protocolVersion: Optional[str] + description: Optional[str] + tags: Optional[list[Union[Tag, AnyDict]]] + security: Optional[SecurityRequirement] + variables: Optional[dict[str, Union[ServerVariable, Reference]]] = None if PYDANTIC_V2: diff --git a/faststream/specification/asyncapi/v2_6_0/schema/tag.py b/faststream/specification/asyncapi/v2_6_0/schema/tag.py index 182c4effd9..a4fdae8d8d 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/tag.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/tag.py @@ -1,14 +1,15 @@ from typing import Optional, Union, overload -import typing_extensions from pydantic import BaseModel +from typing_extensions import Self from faststream._internal._compat import PYDANTIC_V2 from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec -from faststream.specification.asyncapi.v2_6_0.schema.docs import ( - ExternalDocs, - from_spec as docs_from_spec, +from faststream._internal.utils.data import filter_by_dict +from faststream.specification.asyncapi.v2_6_0.schema.docs import ExternalDocs +from faststream.specification.schema.extra import ( + Tag as SpecTag, + TagDict, ) @@ -19,10 +20,10 @@ class Tag(BaseModel): name : name of the tag description : description of the tag (optional) externalDocs : external documentation for the tag (optional) - """ name: str + # Use default values to be able build from dict description: Optional[str] = None externalDocs: Optional[ExternalDocs] = None @@ -34,31 +35,34 @@ class Tag(BaseModel): class Config: extra = "allow" + @overload @classmethod - def from_spec(cls, tag: spec.tag.Tag) -> typing_extensions.Self: - return cls( - name=tag.name, - description=tag.description, - externalDocs=docs_from_spec(tag.externalDocs) if tag.externalDocs else None, - ) - - -@overload -def from_spec(tag: spec.tag.Tag) -> Tag: ... - + def from_spec(cls, tag: SpecTag) -> Self: ... -@overload -def from_spec(tag: spec.tag.TagDict) -> AnyDict: ... + @overload + @classmethod + def from_spec(cls, tag: TagDict) -> Self: ... + @overload + @classmethod + def from_spec(cls, tag: AnyDict) -> AnyDict: ... -@overload -def from_spec(tag: AnyDict) -> AnyDict: ... + @classmethod + def from_spec(cls, tag: Union[SpecTag, TagDict, AnyDict]) -> Union[Self, AnyDict]: + if isinstance(tag, SpecTag): + return cls( + name=tag.name, + description=tag.description, + externalDocs=ExternalDocs.from_spec(tag.external_docs), + ) + tag_data, custom_data = filter_by_dict(TagDict, tag) -def from_spec( - tag: Union[spec.tag.Tag, spec.tag.TagDict, AnyDict], -) -> Union[Tag, AnyDict]: - if isinstance(tag, spec.tag.Tag): - return Tag.from_spec(tag) + if custom_data: + return tag - return dict(tag) + return cls( + name=tag_data.get("name"), + description=tag_data.get("description"), + externalDocs=tag_data.get("external_docs"), + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/utils.py b/faststream/specification/asyncapi/v2_6_0/schema/utils.py index d145abe37d..6d492ffeb5 100644 --- a/faststream/specification/asyncapi/v2_6_0/schema/utils.py +++ b/faststream/specification/asyncapi/v2_6_0/schema/utils.py @@ -6,7 +6,6 @@ class Reference(BaseModel): Attributes: ref : the reference string - """ ref: str = Field(..., alias="$ref") diff --git a/faststream/specification/asyncapi/v3_0_0/facade.py b/faststream/specification/asyncapi/v3_0_0/facade.py index 26e668bd8f..4ce47b6f90 100644 --- a/faststream/specification/asyncapi/v3_0_0/facade.py +++ b/faststream/specification/asyncapi/v3_0_0/facade.py @@ -1,18 +1,24 @@ -from collections.abc import Sequence +from collections.abc import Iterable from typing import TYPE_CHECKING, Any, Optional, Union from faststream.specification.base.specification import Specification from .generate import get_app_schema -from .schema import Schema +from .schema import ApplicationSchema if TYPE_CHECKING: from faststream._internal.basic_types import AnyDict, AnyHttpUrl from faststream._internal.broker.broker import BrokerUsecase - from faststream.specification.schema.contact import Contact, ContactDict - from faststream.specification.schema.docs import ExternalDocs, ExternalDocsDict - from faststream.specification.schema.license import License, LicenseDict - from faststream.specification.schema.tag import Tag, TagDict + from faststream.specification.schema.extra import ( + Contact, + ContactDict, + ExternalDocs, + ExternalDocsDict, + License, + LicenseDict, + Tag, + TagDict, + ) class AsyncAPI3(Specification): @@ -28,7 +34,7 @@ def __init__( contact: Optional[Union["Contact", "ContactDict", "AnyDict"]] = None, license: Optional[Union["License", "LicenseDict", "AnyDict"]] = None, identifier: Optional[str] = None, - tags: Optional[Sequence[Union["Tag", "TagDict", "AnyDict"]]] = None, + tags: Iterable[Union["Tag", "TagDict", "AnyDict"]] = (), external_docs: Optional[ Union["ExternalDocs", "ExternalDocsDict", "AnyDict"] ] = None, @@ -55,7 +61,7 @@ def to_yaml(self) -> str: return self.schema.to_yaml() @property - def schema(self) -> Schema: # type: ignore[override] + def schema(self) -> ApplicationSchema: # type: ignore[override] return get_app_schema( self.broker, title=self.title, diff --git a/faststream/specification/asyncapi/v3_0_0/generate.py b/faststream/specification/asyncapi/v3_0_0/generate.py index b14dca2772..1efc8c4fdc 100644 --- a/faststream/specification/asyncapi/v3_0_0/generate.py +++ b/faststream/specification/asyncapi/v3_0_0/generate.py @@ -5,40 +5,34 @@ from faststream._internal._compat import DEF_KEY from faststream._internal.basic_types import AnyDict, AnyHttpUrl from faststream._internal.constants import ContentTypes -from faststream.specification.asyncapi.utils import clear_key -from faststream.specification.asyncapi.v2_6_0.generate import move_pydantic_refs -from faststream.specification.asyncapi.v2_6_0.schema import ( - Reference, - Tag, - contact_from_spec, - docs_from_spec, - license_from_spec, - tag_from_spec, -) -from faststream.specification.asyncapi.v2_6_0.schema.message import Message +from faststream.specification.asyncapi.utils import clear_key, move_pydantic_refs from faststream.specification.asyncapi.v3_0_0.schema import ( + ApplicationInfo, + ApplicationSchema, Channel, Components, - Info, + Contact, + ExternalDocs, + License, + Message, Operation, - Schema, + Reference, Server, - channel_from_spec, - operation_from_spec, -) -from faststream.specification.asyncapi.v3_0_0.schema.operations import ( - Action, + Tag, ) if TYPE_CHECKING: from faststream._internal.broker.broker import BrokerUsecase from faststream._internal.types import ConnectionType, MsgType - from faststream.specification.schema.contact import Contact, ContactDict - from faststream.specification.schema.docs import ExternalDocs, ExternalDocsDict - from faststream.specification.schema.license import License, LicenseDict - from faststream.specification.schema.tag import ( - Tag as SpecsTag, - TagDict as SpecsTagDict, + from faststream.specification.schema.extra import ( + Contact as SpecContact, + ContactDict, + ExternalDocs as SpecDocs, + ExternalDocsDict, + License as SpecLicense, + LicenseDict, + Tag as SpecTag, + TagDict, ) @@ -50,18 +44,17 @@ def get_app_schema( schema_version: str, description: str, terms_of_service: Optional["AnyHttpUrl"], - contact: Optional[Union["Contact", "ContactDict", "AnyDict"]], - license: Optional[Union["License", "LicenseDict", "AnyDict"]], + contact: Optional[Union["SpecContact", "ContactDict", "AnyDict"]], + license: Optional[Union["SpecLicense", "LicenseDict", "AnyDict"]], identifier: Optional[str], - tags: Optional[Sequence[Union["SpecsTag", "SpecsTagDict", "AnyDict"]]], - external_docs: Optional[Union["ExternalDocs", "ExternalDocsDict", "AnyDict"]], -) -> Schema: + tags: Optional[Sequence[Union["SpecTag", "TagDict", "AnyDict"]]], + external_docs: Optional[Union["SpecDocs", "ExternalDocsDict", "AnyDict"]], +) -> ApplicationSchema: """Get the application schema.""" broker._setup() servers = get_broker_server(broker) - channels = get_broker_channels(broker) - operations = get_broker_operations(broker) + channels, operations = get_broker_channels(broker) messages: dict[str, Message] = {} payloads: dict[str, AnyDict] = {} @@ -86,16 +79,16 @@ def get_app_schema( channel.messages = msgs - return Schema( - info=Info( + return ApplicationSchema( + info=ApplicationInfo( title=title, version=app_version, description=description, termsOfService=terms_of_service, - contact=contact_from_spec(contact) if contact else None, - license=license_from_spec(license) if license else None, - tags=[tag_from_spec(tag) for tag in tags] if tags else None, - externalDocs=docs_from_spec(external_docs) if external_docs else None, + contact=Contact.from_spec(contact), + license=License.from_spec(license), + tags=[Tag.from_spec(tag) for tag in tags] or None, + externalDocs=ExternalDocs.from_spec(external_docs), ), asyncapi=schema_version, defaultContentType=ContentTypes.JSON.value, @@ -121,7 +114,7 @@ def get_broker_server( tags: Optional[list[Union[Tag, AnyDict]]] = None if broker.tags: - tags = [tag_from_spec(tag) for tag in broker.tags] + tags = [Tag.from_spec(tag) for tag in broker.tags] broker_meta: AnyDict = { "protocol": broker.protocol, @@ -152,77 +145,52 @@ def get_broker_server( return servers -def get_broker_operations( - broker: "BrokerUsecase[MsgType, ConnectionType]", -) -> dict[str, Operation]: - """Get the broker operations for an application.""" - operations = {} - - for h in broker._subscribers: - for channel, specs_channel in h.schema().items(): - channel_name = clear_key(channel) - - if specs_channel.subscribe is not None: - operations[f"{channel_name}Subscribe"] = operation_from_spec( - specs_channel.subscribe, - Action.RECEIVE, - channel_name, - ) - - for p in broker._publishers: - for channel, specs_channel in p.schema().items(): - channel_name = clear_key(channel) - - if specs_channel.publish is not None: - operations[f"{channel_name}"] = operation_from_spec( - specs_channel.publish, - Action.SEND, - channel_name, - ) - - return operations - - def get_broker_channels( broker: "BrokerUsecase[MsgType, ConnectionType]", -) -> dict[str, Channel]: +) -> tuple[dict[str, Channel], dict[str, Operation]]: """Get the broker channels for an application.""" channels = {} + operations = {} for sub in broker._subscribers: - channels_schema_v3_0 = {} - for channel_name, specs_channel in sub.schema().items(): - if specs_channel.subscribe: - message = specs_channel.subscribe.message - assert message.title - - *left, right = message.title.split(":") - message.title = ":".join(left) + f":Subscribe{right}" - - # TODO: why we are format just a key? - channels_schema_v3_0[clear_key(channel_name)] = channel_from_spec( - specs_channel, - message, - channel_name, - "SubscribeMessage", - ) - - channels.update(channels_schema_v3_0) + for key, channel in sub.schema().items(): + channel_obj = Channel.from_sub(key, channel) + + channel_key = clear_key(key) + # TODO: add duplication key warning + channels[channel_key] = channel_obj + + operations[f"{channel_key}Subscribe"] = Operation.from_sub( + messages=[ + Reference(**{ + "$ref": f"#/channels/{channel_key}/messages/{msg_name}" + }) + for msg_name in channel_obj.messages + ], + channel=Reference(**{"$ref": f"#/channels/{channel_key}"}), + operation=channel.operation, + ) for pub in broker._publishers: - channels_schema_v3_0 = {} - for channel_name, specs_channel in pub.schema().items(): - if specs_channel.publish: - channels_schema_v3_0[clear_key(channel_name)] = channel_from_spec( - specs_channel, - specs_channel.publish.message, - channel_name, - "Message", - ) - - channels.update(channels_schema_v3_0) - - return channels + for key, channel in pub.schema().items(): + channel_obj = Channel.from_pub(key, channel) + + channel_key = clear_key(key) + # TODO: add duplication key warning + channels[channel_key] = channel_obj + + operations[channel_key] = Operation.from_pub( + messages=[ + Reference(**{ + "$ref": f"#/channels/{channel_key}/messages/{msg_name}" + }) + for msg_name in channel_obj.messages + ], + channel=Reference(**{"$ref": f"#/channels/{channel_key}"}), + operation=channel.operation, + ) + + return channels, operations def _resolve_msg_payloads( diff --git a/faststream/specification/asyncapi/v3_0_0/schema/__init__.py b/faststream/specification/asyncapi/v3_0_0/schema/__init__.py index ef2cfe21b7..e0cbcbd7b2 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/__init__.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/__init__.py @@ -1,23 +1,31 @@ -from .channels import ( - Channel, - from_spec as channel_from_spec, -) +from .channels import Channel from .components import Components -from .info import Info -from .operations import ( - Operation, - from_spec as operation_from_spec, -) -from .schema import Schema -from .servers import Server +from .contact import Contact +from .docs import ExternalDocs +from .info import ApplicationInfo +from .license import License +from .message import CorrelationId, Message +from .operations import Operation +from .schema import ApplicationSchema +from .servers import Server, ServerVariable +from .tag import Tag +from .utils import Parameter, Reference __all__ = ( + "ApplicationInfo", + "ApplicationSchema", + "Channel", "Channel", "Components", - "Info", + "Contact", + "CorrelationId", + "ExternalDocs", + "License", + "Message", "Operation", - "Schema", + "Parameter", + "Reference", "Server", - "channel_from_spec", - "operation_from_spec", + "ServerVariable", + "Tag", ) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/bindings/__init__.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/__init__.py index d477f704cd..c304608c5b 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/bindings/__init__.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/__init__.py @@ -1,11 +1,9 @@ from .main import ( + ChannelBinding, OperationBinding, - channel_binding_from_spec, - operation_binding_from_spec, ) __all__ = ( + "ChannelBinding", "OperationBinding", - "channel_binding_from_spec", - "operation_binding_from_spec", ) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/__init__.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/__init__.py index 96c7406698..8555fd981a 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/__init__.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/__init__.py @@ -1,11 +1,7 @@ -from .channel import from_spec as channel_binding_from_spec -from .operation import ( - OperationBinding, - from_spec as operation_binding_from_spec, -) +from .channel import ChannelBinding +from .operation import OperationBinding __all__ = ( + "ChannelBinding", "OperationBinding", - "channel_binding_from_spec", - "operation_binding_from_spec", ) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/channel.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/channel.py index a31498ee5f..ecab8e4a17 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/channel.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/channel.py @@ -1,21 +1,7 @@ -from faststream.specification import schema as spec -from faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp import ChannelBinding -from faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp.channel import ( - Exchange, - Queue, +from faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp import ( + ChannelBinding as V2Binding, ) -def from_spec(binding: spec.bindings.amqp.ChannelBinding) -> ChannelBinding: - return ChannelBinding( - **{ - "is": binding.is_, - "bindingVersion": "0.3.0", - "queue": Queue.from_spec(binding.queue) - if binding.queue is not None - else None, - "exchange": Exchange.from_spec(binding.exchange) - if binding.exchange is not None - else None, - }, - ) +class ChannelBinding(V2Binding): + bindingVersion: str = "0.3.0" diff --git a/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/operation.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/operation.py index 1357dd325f..77ba8356a0 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/operation.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/operation.py @@ -5,41 +5,46 @@ from typing import Optional -from pydantic import BaseModel, PositiveInt from typing_extensions import Self -from faststream.specification import schema as spec +from faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp import ( + OperationBinding as V2Binding, +) +from faststream.specification.schema.bindings import amqp -class OperationBinding(BaseModel): - """A class to represent an operation binding. - - Attributes: - cc : optional string representing the cc - ack : boolean indicating if the operation is acknowledged - replyTo : optional dictionary representing the replyTo - bindingVersion : string representing the binding version - """ - +class OperationBinding(V2Binding): cc: Optional[list[str]] = None - ack: bool = True - replyTo: Optional[str] = None - deliveryMode: Optional[int] = None - mandatory: Optional[bool] = None - priority: Optional[PositiveInt] = None bindingVersion: str = "0.3.0" @classmethod - def from_spec(cls, binding: spec.bindings.amqp.OperationBinding) -> Self: + def from_sub(cls, binding: Optional[amqp.OperationBinding]) -> Optional[Self]: + if not binding: + return None + return cls( - cc=[binding.cc] if binding.cc is not None else None, + cc=[binding.routing_key] + if (binding.routing_key and binding.exchange.is_respect_routing_key) + else None, ack=binding.ack, - replyTo=binding.replyTo, - deliveryMode=binding.deliveryMode, + replyTo=binding.reply_to, + deliveryMode=None if binding.persist is None else int(binding.persist) + 1, mandatory=binding.mandatory, priority=binding.priority, ) + @classmethod + def from_pub(cls, binding: Optional[amqp.OperationBinding]) -> Optional[Self]: + if not binding: + return None -def from_spec(binding: spec.bindings.amqp.OperationBinding) -> OperationBinding: - return OperationBinding.from_spec(binding) + return cls( + cc=None + if (not binding.routing_key or not binding.exchange.is_respect_routing_key) + else [binding.routing_key], + ack=binding.ack, + replyTo=binding.reply_to, + deliveryMode=None if binding.persist is None else int(binding.persist) + 1, + mandatory=binding.mandatory, + priority=binding.priority, + ) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/bindings/kafka.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/kafka.py new file mode 100644 index 0000000000..5605abeefa --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/kafka.py @@ -0,0 +1,9 @@ +from faststream.specification.asyncapi.v2_6_0.schema.bindings.kafka import ( + ChannelBinding, + OperationBinding, +) + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/__init__.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/__init__.py index 96c7406698..8555fd981a 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/__init__.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/__init__.py @@ -1,11 +1,7 @@ -from .channel import from_spec as channel_binding_from_spec -from .operation import ( - OperationBinding, - from_spec as operation_binding_from_spec, -) +from .channel import ChannelBinding +from .operation import OperationBinding __all__ = ( + "ChannelBinding", "OperationBinding", - "channel_binding_from_spec", - "operation_binding_from_spec", ) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/channel.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/channel.py index 41aef76aaa..c7552a11d1 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/channel.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/channel.py @@ -1,17 +1,100 @@ -from faststream.specification import schema as spec -from faststream.specification.asyncapi.v2_6_0.schema.bindings import ChannelBinding -from faststream.specification.asyncapi.v2_6_0.schema.bindings.main import ( - channel_binding_from_spec, -) -from faststream.specification.asyncapi.v3_0_0.schema.bindings.amqp import ( - channel_binding_from_spec as amqp_channel_binding_from_spec, +from typing import Optional + +from pydantic import BaseModel +from typing_extensions import Self + +from faststream._internal._compat import PYDANTIC_V2 +from faststream.specification.asyncapi.v3_0_0.schema.bindings import ( + amqp as amqp_bindings, + kafka as kafka_bindings, + nats as nats_bindings, + redis as redis_bindings, + sqs as sqs_bindings, ) +from faststream.specification.schema.bindings import ChannelBinding as SpecBinding + + +class ChannelBinding(BaseModel): + """A class to represent channel bindings. + + Attributes: + amqp : AMQP channel binding (optional) + kafka : Kafka channel binding (optional) + sqs : SQS channel binding (optional) + nats : NATS channel binding (optional) + redis : Redis channel binding (optional) + """ + + amqp: Optional[amqp_bindings.ChannelBinding] = None + kafka: Optional[kafka_bindings.ChannelBinding] = None + sqs: Optional[sqs_bindings.ChannelBinding] = None + nats: Optional[nats_bindings.ChannelBinding] = None + redis: Optional[redis_bindings.ChannelBinding] = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @classmethod + def from_sub(cls, binding: Optional[SpecBinding]) -> Optional[Self]: + if binding is None: + return None + + if binding.amqp and ( + amqp := amqp_bindings.ChannelBinding.from_sub(binding.amqp) + ): + return cls(amqp=amqp) + + if binding.kafka and ( + kafka := kafka_bindings.ChannelBinding.from_sub(binding.kafka) + ): + return cls(kafka=kafka) + + if binding.nats and ( + nats := nats_bindings.ChannelBinding.from_sub(binding.nats) + ): + return cls(nats=nats) + + if binding.redis and ( + redis := redis_bindings.ChannelBinding.from_sub(binding.redis) + ): + return cls(redis=redis) + + if binding.sqs and (sqs := sqs_bindings.ChannelBinding.from_sub(binding.sqs)): + return cls(sqs=sqs) + + return None + + @classmethod + def from_pub(cls, binding: Optional[SpecBinding]) -> Optional[Self]: + if binding is None: + return None + + if binding.amqp and ( + amqp := amqp_bindings.ChannelBinding.from_pub(binding.amqp) + ): + return cls(amqp=amqp) + + if binding.kafka and ( + kafka := kafka_bindings.ChannelBinding.from_pub(binding.kafka) + ): + return cls(kafka=kafka) + if binding.nats and ( + nats := nats_bindings.ChannelBinding.from_pub(binding.nats) + ): + return cls(nats=nats) -def from_spec(binding: spec.bindings.ChannelBinding) -> ChannelBinding: - channel_binding = channel_binding_from_spec(binding) + if binding.redis and ( + redis := redis_bindings.ChannelBinding.from_pub(binding.redis) + ): + return cls(redis=redis) - if binding.amqp: - channel_binding.amqp = amqp_channel_binding_from_spec(binding.amqp) + if binding.sqs and (sqs := sqs_bindings.ChannelBinding.from_pub(binding.sqs)): + return cls(sqs=sqs) - return channel_binding + return None diff --git a/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/operation.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/operation.py index 6d0a70069e..fc37c3dc75 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/operation.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/operation.py @@ -4,16 +4,14 @@ from typing_extensions import Self from faststream._internal._compat import PYDANTIC_V2 -from faststream.specification import schema as spec -from faststream.specification.asyncapi.v2_6_0.schema.bindings import ( +from faststream.specification.asyncapi.v3_0_0.schema.bindings import ( + amqp as amqp_bindings, kafka as kafka_bindings, nats as nats_bindings, redis as redis_bindings, sqs as sqs_bindings, ) -from faststream.specification.asyncapi.v3_0_0.schema.bindings import ( - amqp as amqp_bindings, -) +from faststream.specification.schema.bindings import OperationBinding as SpecBinding class OperationBinding(BaseModel): @@ -25,7 +23,6 @@ class OperationBinding(BaseModel): sqs : SQS operation binding (optional) nats : NATS operation binding (optional) redis : Redis operation binding (optional) - """ amqp: Optional[amqp_bindings.OperationBinding] = None @@ -43,25 +40,61 @@ class Config: extra = "allow" @classmethod - def from_spec(cls, binding: spec.bindings.OperationBinding) -> Self: - return cls( - amqp=amqp_bindings.operation_binding_from_spec(binding.amqp) - if binding.amqp is not None - else None, - kafka=kafka_bindings.operation_binding_from_spec(binding.kafka) - if binding.kafka is not None - else None, - sqs=sqs_bindings.operation_binding_from_spec(binding.sqs) - if binding.sqs is not None - else None, - nats=nats_bindings.operation_binding_from_spec(binding.nats) - if binding.nats is not None - else None, - redis=redis_bindings.operation_binding_from_spec(binding.redis) - if binding.redis is not None - else None, - ) - - -def from_spec(binding: spec.bindings.OperationBinding) -> OperationBinding: - return OperationBinding.from_spec(binding) + def from_sub(cls, binding: Optional[SpecBinding]) -> Optional[Self]: + if binding is None: + return None + + if binding.amqp and ( + amqp := amqp_bindings.OperationBinding.from_sub(binding.amqp) + ): + return cls(amqp=amqp) + + if binding.kafka and ( + kafka := kafka_bindings.OperationBinding.from_sub(binding.kafka) + ): + return cls(kafka=kafka) + + if binding.nats and ( + nats := nats_bindings.OperationBinding.from_sub(binding.nats) + ): + return cls(nats=nats) + + if binding.redis and ( + redis := redis_bindings.OperationBinding.from_sub(binding.redis) + ): + return cls(redis=redis) + + if binding.sqs and (sqs := sqs_bindings.OperationBinding.from_sub(binding.sqs)): + return cls(sqs=sqs) + + return None + + @classmethod + def from_pub(cls, binding: Optional[SpecBinding]) -> Optional[Self]: + if binding is None: + return None + + if binding.amqp and ( + amqp := amqp_bindings.OperationBinding.from_pub(binding.amqp) + ): + return cls(amqp=amqp) + + if binding.kafka and ( + kafka := kafka_bindings.OperationBinding.from_pub(binding.kafka) + ): + return cls(kafka=kafka) + + if binding.nats and ( + nats := nats_bindings.OperationBinding.from_pub(binding.nats) + ): + return cls(nats=nats) + + if binding.redis and ( + redis := redis_bindings.OperationBinding.from_pub(binding.redis) + ): + return cls(redis=redis) + + if binding.sqs and (sqs := sqs_bindings.OperationBinding.from_pub(binding.sqs)): + return cls(sqs=sqs) + + return None diff --git a/faststream/specification/asyncapi/v3_0_0/schema/bindings/nats.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/nats.py new file mode 100644 index 0000000000..21d5c46926 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/nats.py @@ -0,0 +1,9 @@ +from faststream.specification.asyncapi.v2_6_0.schema.bindings.nats import ( + ChannelBinding, + OperationBinding, +) + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/bindings/redis.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/redis.py new file mode 100644 index 0000000000..26d44644f7 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/redis.py @@ -0,0 +1,9 @@ +from faststream.specification.asyncapi.v2_6_0.schema.bindings.redis import ( + ChannelBinding, + OperationBinding, +) + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/bindings/sqs.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/sqs.py new file mode 100644 index 0000000000..e437a1cc58 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/sqs.py @@ -0,0 +1,9 @@ +from faststream.specification.asyncapi.v2_6_0.schema.bindings.sqs import ( + ChannelBinding, + OperationBinding, +) + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/channels.py b/faststream/specification/asyncapi/v3_0_0/schema/channels.py index 3a5ccd40bb..c0a2dbe553 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/channels.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/channels.py @@ -4,16 +4,11 @@ from typing_extensions import Self from faststream._internal._compat import PYDANTIC_V2 -from faststream.specification import schema as spec -from faststream.specification.asyncapi.v2_6_0.schema.bindings import ChannelBinding -from faststream.specification.asyncapi.v2_6_0.schema.message import ( - Message, - from_spec as message_from_spec, -) -from faststream.specification.asyncapi.v2_6_0.schema.utils import Reference -from faststream.specification.asyncapi.v3_0_0.schema.bindings.main import ( - channel_binding_from_spec, -) +from faststream.specification.asyncapi.v3_0_0.schema.bindings import ChannelBinding +from faststream.specification.asyncapi.v3_0_0.schema.message import Message +from faststream.specification.schema import PublisherSpec, SubscriberSpec + +from .utils import Reference class Channel(BaseModel): @@ -29,7 +24,6 @@ class Channel(BaseModel): Configurations: model_config : configuration for the model (only applicable for Pydantic version 2) Config : configuration for the class (only applicable for Pydantic version 1) - """ address: str @@ -50,30 +44,31 @@ class Config: extra = "allow" @classmethod - def from_spec( - cls, - channel: spec.channel.Channel, - message: spec.message.Message, - channel_name: str, - message_name: str, - ) -> Self: + def from_sub(cls, address: str, subscriber: SubscriberSpec) -> Self: + message = subscriber.operation.message + assert message.title + + *left, right = message.title.split(":") + message.title = ":".join((*left, f"Subscribe{right}")) + return cls( - address=channel_name, + description=subscriber.description, + address=address, messages={ - message_name: message_from_spec(message), + "SubscribeMessage": Message.from_spec(message), }, - description=channel.description, - servers=channel.servers, - bindings=channel_binding_from_spec(channel.bindings) - if channel.bindings - else None, + bindings=ChannelBinding.from_sub(subscriber.bindings), + servers=None, ) - -def from_spec( - channel: spec.channel.Channel, - message: spec.message.Message, - channel_name: str, - message_name: str, -) -> Channel: - return Channel.from_spec(channel, message, channel_name, message_name) + @classmethod + def from_pub(cls, address: str, publisher: PublisherSpec) -> Self: + return cls( + description=publisher.description, + address=address, + messages={ + "Message": Message.from_spec(publisher.operation.message), + }, + bindings=ChannelBinding.from_pub(publisher.bindings), + servers=None, + ) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/contact.py b/faststream/specification/asyncapi/v3_0_0/schema/contact.py new file mode 100644 index 0000000000..c42e750b28 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/contact.py @@ -0,0 +1,3 @@ +from faststream.specification.asyncapi.v2_6_0.schema import Contact + +__all__ = ("Contact",) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/docs.py b/faststream/specification/asyncapi/v3_0_0/schema/docs.py new file mode 100644 index 0000000000..0a71688697 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/docs.py @@ -0,0 +1,3 @@ +from faststream.specification.asyncapi.v2_6_0.schema import ExternalDocs + +__all__ = ("ExternalDocs",) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/info.py b/faststream/specification/asyncapi/v3_0_0/schema/info.py index 6d15c9e4dc..c9303e690c 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/info.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/info.py @@ -14,16 +14,16 @@ License, Tag, ) -from faststream.specification.base.info import BaseInfo +from faststream.specification.base.info import BaseApplicationInfo -class Info(BaseInfo): - """A class to represent information. +class ApplicationInfo(BaseApplicationInfo): + """A class to represent application information. Attributes: - termsOfService : terms of service for the information (default: None) - contact : contact information for the information (default: None) - license : license information for the information (default: None) + termsOfService : terms of service for the information + contact : contact information for the information + license : license information for the information tags : optional list of tags externalDocs : optional external documentation """ diff --git a/faststream/specification/asyncapi/v3_0_0/schema/license.py b/faststream/specification/asyncapi/v3_0_0/schema/license.py new file mode 100644 index 0000000000..44ee4b2813 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/license.py @@ -0,0 +1,3 @@ +from faststream.specification.asyncapi.v2_6_0.schema import License + +__all__ = ("License",) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/message.py b/faststream/specification/asyncapi/v3_0_0/schema/message.py new file mode 100644 index 0000000000..fa665082e9 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/message.py @@ -0,0 +1,6 @@ +from faststream.specification.asyncapi.v2_6_0.schema.message import ( + CorrelationId, + Message, +) + +__all__ = ("CorrelationId", "Message") diff --git a/faststream/specification/asyncapi/v3_0_0/schema/operations.py b/faststream/specification/asyncapi/v3_0_0/schema/operations.py index ffc647674a..8afff3c5c6 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/operations.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/operations.py @@ -1,21 +1,17 @@ from enum import Enum from typing import Optional, Union -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing_extensions import Self from faststream._internal._compat import PYDANTIC_V2 from faststream._internal.basic_types import AnyDict -from faststream.specification import schema as spec -from faststream.specification.asyncapi.v2_6_0.schema.tag import Tag -from faststream.specification.asyncapi.v2_6_0.schema.utils import ( - Reference, -) -from faststream.specification.asyncapi.v3_0_0.schema.bindings import OperationBinding -from faststream.specification.asyncapi.v3_0_0.schema.bindings.main import ( - operation_binding_from_spec, -) -from faststream.specification.asyncapi.v3_0_0.schema.channels import Channel +from faststream.specification.schema.operation import Operation as OperationSpec + +from .bindings import OperationBinding +from .channels import Channel +from .tag import Tag +from .utils import Reference class Action(str, Enum): @@ -27,24 +23,24 @@ class Operation(BaseModel): """A class to represent an operation. Attributes: - operationId : ID of the operation + operation_id : ID of the operation summary : summary of the operation description : description of the operation bindings : bindings of the operation message : message of the operation security : security details of the operation tags : tags associated with the operation - """ action: Action + channel: Union[Channel, Reference] + summary: Optional[str] = None description: Optional[str] = None bindings: Optional[OperationBinding] = None - messages: list[Reference] - channel: Union[Channel, Reference] + messages: list[Reference] = Field(default_factory=list) security: Optional[dict[str, list[str]]] = None @@ -62,38 +58,37 @@ class Config: extra = "allow" @classmethod - def from_spec( + def from_sub( cls, - operation: spec.operation.Operation, - action: Action, - channel_name: str, + messages: list[Reference], + channel: Reference, + operation: OperationSpec, ) -> Self: return cls( - action=action, - summary=operation.summary, - description=operation.description, - bindings=operation_binding_from_spec(operation.bindings) - if operation.bindings - else None, - messages=[ - Reference( - **{ - "$ref": f"#/channels/{channel_name}/messages/SubscribeMessage" - if action is Action.RECEIVE - else f"#/channels/{channel_name}/messages/Message", - }, - ), - ], - channel=Reference( - **{"$ref": f"#/channels/{channel_name}"}, - ), - security=operation.security, + action=Action.RECEIVE, + messages=messages, + channel=channel, + bindings=OperationBinding.from_sub(operation.bindings), + summary=None, + description=None, + security=None, + tags=None, ) - -def from_spec( - operation: spec.operation.Operation, - action: Action, - channel_name: str, -) -> Operation: - return Operation.from_spec(operation, action, channel_name) + @classmethod + def from_pub( + cls, + messages: list[Reference], + channel: Reference, + operation: OperationSpec, + ) -> Self: + return cls( + action=Action.SEND, + messages=messages, + channel=channel, + bindings=OperationBinding.from_pub(operation.bindings), + summary=None, + description=None, + security=None, + tags=None, + ) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/schema.py b/faststream/specification/asyncapi/v3_0_0/schema/schema.py index ad60b8bfae..dc894ecb4e 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/schema.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/schema.py @@ -1,15 +1,17 @@ from typing import Literal, Optional, Union +from pydantic import Field + from faststream.specification.asyncapi.v3_0_0.schema.channels import Channel from faststream.specification.asyncapi.v3_0_0.schema.components import Components -from faststream.specification.asyncapi.v3_0_0.schema.info import Info +from faststream.specification.asyncapi.v3_0_0.schema.info import ApplicationInfo from faststream.specification.asyncapi.v3_0_0.schema.operations import Operation from faststream.specification.asyncapi.v3_0_0.schema.servers import Server -from faststream.specification.base.schema import BaseSchema +from faststream.specification.base.schema import BaseApplicationSchema -class Schema(BaseSchema): - """A class to represent a schema. +class ApplicationSchema(BaseApplicationSchema): + """A class to represent an application schema. Attributes: asyncapi : version of the async API @@ -21,12 +23,12 @@ class Schema(BaseSchema): components : optional components of the schema """ - info: Info + info: ApplicationInfo asyncapi: Union[Literal["3.0.0"], str] = "3.0.0" id: Optional[str] = None defaultContentType: Optional[str] = None servers: Optional[dict[str, Server]] = None - channels: dict[str, Channel] - operations: dict[str, Operation] + channels: dict[str, Channel] = Field(default_factory=dict) + operations: dict[str, Operation] = Field(default_factory=dict) components: Optional[Components] = None diff --git a/faststream/specification/asyncapi/v3_0_0/schema/tag.py b/faststream/specification/asyncapi/v3_0_0/schema/tag.py new file mode 100644 index 0000000000..e16c4f61cd --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/tag.py @@ -0,0 +1,3 @@ +from faststream.specification.asyncapi.v2_6_0.schema import Tag + +__all__ = ("Tag",) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/utils.py b/faststream/specification/asyncapi/v3_0_0/schema/utils.py new file mode 100644 index 0000000000..c53f3ce1a0 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/utils.py @@ -0,0 +1,6 @@ +from faststream.specification.asyncapi.v2_6_0.schema import Parameter, Reference + +__all__ = ( + "Parameter", + "Reference", +) diff --git a/faststream/specification/base/info.py b/faststream/specification/base/info.py index 79e5164de7..6e282dc19e 100644 --- a/faststream/specification/base/info.py +++ b/faststream/specification/base/info.py @@ -3,18 +3,18 @@ from faststream._internal._compat import PYDANTIC_V2 -class BaseInfo(BaseModel): +class BaseApplicationInfo(BaseModel): """A class to represent basic application information. Attributes: title : application title - version : application version (default: "1.0.0") - description : application description (default: "") + version : application version + description : application description """ title: str - version: str = "1.0.0" - description: str = "" + version: str + description: str if PYDANTIC_V2: model_config = {"extra": "allow"} diff --git a/faststream/specification/base/proto.py b/faststream/specification/base/proto.py deleted file mode 100644 index 42d118c46c..0000000000 --- a/faststream/specification/base/proto.py +++ /dev/null @@ -1,47 +0,0 @@ -from abc import abstractmethod -from typing import Any, Optional, Protocol - -from faststream.specification.schema.channel import Channel - - -class SpecificationEndpoint(Protocol): - """A class representing an asynchronous API operation.""" - - title_: Optional[str] - description_: Optional[str] - include_in_schema: bool - - @property - def name(self) -> str: - """Returns the name of the API operation.""" - return self.title_ or self.get_name() - - @abstractmethod - def get_name(self) -> str: - """Name property fallback.""" - raise NotImplementedError - - @property - def description(self) -> Optional[str]: - """Returns the description of the API operation.""" - return self.description_ or self.get_description() - - def get_description(self) -> Optional[str]: - """Description property fallback.""" - return None - - def schema(self) -> dict[str, Channel]: - """Returns the schema of the API operation as a dictionary of channel names and channel objects.""" - if self.include_in_schema: - return self.get_schema() - return {} - - @abstractmethod - def get_schema(self) -> dict[str, Channel]: - """Generate AsyncAPI schema.""" - raise NotImplementedError - - @abstractmethod - def get_payloads(self) -> Any: - """Generate AsyncAPI payloads.""" - raise NotImplementedError diff --git a/faststream/specification/base/schema.py b/faststream/specification/base/schema.py index 914b389bc2..828e1699b7 100644 --- a/faststream/specification/base/schema.py +++ b/faststream/specification/base/schema.py @@ -4,11 +4,11 @@ from faststream._internal._compat import model_to_json, model_to_jsonable -from .info import BaseInfo +from .info import BaseApplicationInfo -class BaseSchema(BaseModel): - """A class to represent a Pydantic-serializable schema. +class BaseApplicationSchema(BaseModel): + """A class to represent a Pydantic-serializable application schema. Attributes: info : information about the schema @@ -19,7 +19,7 @@ class BaseSchema(BaseModel): to_yaml() -> str: Convert the schema to a YAML string. """ - info: BaseInfo + info: BaseApplicationInfo def to_jsonable(self) -> Any: """Convert the schema to a JSON-serializable object.""" diff --git a/faststream/specification/base/specification.py b/faststream/specification/base/specification.py index d9dc0fcf14..0c3946e76f 100644 --- a/faststream/specification/base/specification.py +++ b/faststream/specification/base/specification.py @@ -1,11 +1,11 @@ from typing import Any, Protocol, runtime_checkable -from .schema import BaseSchema +from .schema import BaseApplicationSchema @runtime_checkable class Specification(Protocol): - schema: BaseSchema + schema: BaseApplicationSchema def to_json(self) -> str: return self.schema.to_json() diff --git a/faststream/specification/proto/__init__.py b/faststream/specification/proto/__init__.py new file mode 100644 index 0000000000..3189e7cc8f --- /dev/null +++ b/faststream/specification/proto/__init__.py @@ -0,0 +1,4 @@ +from .broker import ServerSpecification +from .endpoint import EndpointSpecification + +__all__ = ("EndpointSpecification", "ServerSpecification") diff --git a/faststream/specification/proto/broker.py b/faststream/specification/proto/broker.py new file mode 100644 index 0000000000..225393b24e --- /dev/null +++ b/faststream/specification/proto/broker.py @@ -0,0 +1,14 @@ +from collections.abc import Iterable +from typing import Optional, Protocol, Union + +from faststream.security import BaseSecurity +from faststream.specification.schema.extra import Tag, TagDict + + +class ServerSpecification(Protocol): + url: Union[str, list[str]] + protocol: Optional[str] + protocol_version: Optional[str] + description: Optional[str] + tags: Iterable[Union[Tag, TagDict]] + security: Optional[BaseSecurity] diff --git a/faststream/specification/proto/endpoint.py b/faststream/specification/proto/endpoint.py new file mode 100644 index 0000000000..380acb1071 --- /dev/null +++ b/faststream/specification/proto/endpoint.py @@ -0,0 +1,62 @@ +from abc import ABC, abstractmethod +from typing import Any, Generic, Optional, TypeVar + +T = TypeVar("T") + + +class EndpointSpecification(ABC, Generic[T]): + """A class representing an asynchronous API operation: Pub or Sub.""" + + title_: Optional[str] + description_: Optional[str] + include_in_schema: bool + + def __init__( + self, + *args: Any, + title_: Optional[str], + description_: Optional[str], + include_in_schema: bool, + **kwargs: Any, + ) -> None: + self.title_ = title_ + self.description_ = description_ + self.include_in_schema = include_in_schema + + # Call next base class parent init + super().__init__(*args, **kwargs) + + @property + def name(self) -> str: + """Returns the name of the API operation.""" + return self.title_ or self.get_default_name() + + @abstractmethod + def get_default_name(self) -> str: + """Name property fallback.""" + raise NotImplementedError + + @property + def description(self) -> Optional[str]: + """Returns the description of the API operation.""" + return self.description_ or self.get_default_description() + + def get_default_description(self) -> Optional[str]: + """Description property fallback.""" + return None + + def schema(self) -> dict[str, T]: + """Returns the schema of the API operation as a dictionary of channel names and channel objects.""" + if self.include_in_schema: + return self.get_schema() + return {} + + @abstractmethod + def get_schema(self) -> dict[str, T]: + """Generate AsyncAPI schema.""" + raise NotImplementedError + + @abstractmethod + def get_payloads(self) -> Any: + """Generate AsyncAPI payloads.""" + raise NotImplementedError diff --git a/faststream/specification/schema/__init__.py b/faststream/specification/schema/__init__.py index a2ec26fa7a..009a6a63d7 100644 --- a/faststream/specification/schema/__init__.py +++ b/faststream/specification/schema/__init__.py @@ -1,34 +1,29 @@ -from . import ( - bindings, - channel, - contact, - docs, - info, - license, - message, - operation, - security, - tag, +from .extra import ( + Contact, + ContactDict, + ExternalDocs, + ExternalDocsDict, + License, + LicenseDict, + Tag, + TagDict, ) -from .contact import Contact -from .docs import ExternalDocs -from .license import License -from .tag import Tag +from .message import Message +from .operation import Operation +from .publisher import PublisherSpec +from .subscriber import SubscriberSpec __all__ = ( "Contact", + "ContactDict", "ExternalDocs", + "ExternalDocsDict", "License", + "LicenseDict", + "Message", + "Operation", + "PublisherSpec", + "SubscriberSpec", "Tag", - # module aliases - "bindings", - "channel", - "contact", - "docs", - "info", - "license", - "message", - "operation", - "security", - "tag", + "TagDict", ) diff --git a/faststream/specification/schema/bindings/amqp.py b/faststream/specification/schema/bindings/amqp.py index 42f29dd1c8..f15201bb8e 100644 --- a/faststream/specification/schema/bindings/amqp.py +++ b/faststream/specification/schema/bindings/amqp.py @@ -1,43 +1,29 @@ -"""AsyncAPI AMQP bindings. - -References: https://github.com/asyncapi/bindings/tree/master/amqp -""" - from dataclasses import dataclass -from typing import Literal, Optional +from typing import TYPE_CHECKING, Literal, Optional + +if TYPE_CHECKING: + from faststream.rabbit.schemas import RabbitExchange, RabbitQueue @dataclass class Queue: - """A class to represent a queue. - - Attributes: - name : name of the queue - durable : indicates if the queue is durable - exclusive : indicates if the queue is exclusive - autoDelete : indicates if the queue should be automatically deleted - vhost : virtual host of the queue (default is "/") - """ - name: str durable: bool exclusive: bool - autoDelete: bool - vhost: str = "/" + auto_delete: bool + + @classmethod + def from_queue(cls, queue: "RabbitQueue") -> "Queue": + return cls( + name=queue.name, + durable=queue.durable, + exclusive=queue.exclusive, + auto_delete=queue.auto_delete, + ) @dataclass class Exchange: - """A class to represent an exchange. - - Attributes: - name : name of the exchange (optional) - type : type of the exchange, can be one of "default", "direct", "topic", "fanout", "headers" - durable : whether the exchange is durable (optional) - autoDelete : whether the exchange is automatically deleted (optional) - vhost : virtual host of the exchange, default is "/" - """ - type: Literal[ "default", "direct", @@ -51,40 +37,43 @@ class Exchange: name: Optional[str] = None durable: Optional[bool] = None - autoDelete: Optional[bool] = None - vhost: str = "/" + auto_delete: Optional[bool] = None + + @classmethod + def from_exchange(cls, exchange: "RabbitExchange") -> "Exchange": + if not exchange.name: + return cls(type="default") + return cls( + type=exchange.type.value, + name=exchange.name, + durable=exchange.durable, + auto_delete=exchange.auto_delete, + ) + + @property + def is_respect_routing_key(self) -> bool: + """Is exchange respects routing key or not.""" + return self.type in { + "default", + "direct", + "topic", + } @dataclass class ChannelBinding: - """A class to represent channel binding. - - Attributes: - is_ : Type of binding, can be "queue" or "routingKey" - bindingVersion : Version of the binding - queue : Optional queue object - exchange : Optional exchange object - """ - - is_: Literal["queue", "routingKey"] - queue: Optional[Queue] = None - exchange: Optional[Exchange] = None + queue: Queue + exchange: Exchange + virtual_host: str @dataclass class OperationBinding: - """A class to represent an operation binding. - - Attributes: - cc : optional string representing the cc - ack : boolean indicating if the operation is acknowledged - replyTo : optional dictionary representing the replyTo - bindingVersion : string representing the binding version - """ - - cc: Optional[str] = None - ack: bool = True - replyTo: Optional[str] = None - deliveryMode: Optional[int] = None - mandatory: Optional[bool] = None - priority: Optional[int] = None + routing_key: Optional[str] + queue: Queue + exchange: Exchange + ack: bool + reply_to: Optional[str] + persist: Optional[bool] + mandatory: Optional[bool] + priority: Optional[int] diff --git a/faststream/specification/schema/bindings/kafka.py b/faststream/specification/schema/bindings/kafka.py index 142a2a5285..fc9d0867c8 100644 --- a/faststream/specification/schema/bindings/kafka.py +++ b/faststream/specification/schema/bindings/kafka.py @@ -15,13 +15,11 @@ class ChannelBinding: topic : optional string representing the topic partitions : optional positive integer representing the number of partitions replicas : optional positive integer representing the number of replicas - bindingVersion : string representing the binding version """ - topic: Optional[str] = None - partitions: Optional[int] = None - replicas: Optional[int] = None - bindingVersion: str = "0.4.0" + topic: Optional[str] + partitions: Optional[int] + replicas: Optional[int] # TODO: # topicConfiguration @@ -32,13 +30,11 @@ class OperationBinding: """A class to represent an operation binding. Attributes: - groupId : optional dictionary representing the group ID - clientId : optional dictionary representing the client ID - replyTo : optional dictionary representing the reply-to - bindingVersion : version of the binding (default: "0.4.0") + group_id : optional dictionary representing the group ID + client_id : optional dictionary representing the client ID + reply_to : optional dictionary representing the reply-to """ - groupId: Optional[dict[str, Any]] = None - clientId: Optional[dict[str, Any]] = None - replyTo: Optional[dict[str, Any]] = None - bindingVersion: str = "0.4.0" + group_id: Optional[dict[str, Any]] + client_id: Optional[dict[str, Any]] + reply_to: Optional[dict[str, Any]] diff --git a/faststream/specification/schema/bindings/nats.py b/faststream/specification/schema/bindings/nats.py index 034efada4e..412f29d557 100644 --- a/faststream/specification/schema/bindings/nats.py +++ b/faststream/specification/schema/bindings/nats.py @@ -14,12 +14,10 @@ class ChannelBinding: Attributes: subject : subject of the channel binding queue : optional queue for the channel binding - bindingVersion : version of the channel binding, default is "custom" """ subject: str - queue: Optional[str] = None - bindingVersion: str = "custom" + queue: Optional[str] @dataclass @@ -27,9 +25,7 @@ class OperationBinding: """A class to represent an operation binding. Attributes: - replyTo : optional dictionary containing reply information - bindingVersion : version of the binding (default is "custom") + reply_to : optional dictionary containing reply information """ - replyTo: Optional[dict[str, Any]] = None - bindingVersion: str = "custom" + reply_to: Optional[dict[str, Any]] diff --git a/faststream/specification/schema/bindings/redis.py b/faststream/specification/schema/bindings/redis.py index c1b3e138a0..17287aa5e4 100644 --- a/faststream/specification/schema/bindings/redis.py +++ b/faststream/specification/schema/bindings/redis.py @@ -14,14 +14,12 @@ class ChannelBinding: Attributes: channel : the channel name method : the method used for binding (ssubscribe, psubscribe, subscribe) - bindingVersion : the version of the binding """ channel: str method: Optional[str] = None group_name: Optional[str] = None consumer_name: Optional[str] = None - bindingVersion: str = "custom" @dataclass @@ -29,9 +27,7 @@ class OperationBinding: """A class to represent an operation binding. Attributes: - replyTo : optional dictionary containing reply information - bindingVersion : version of the binding (default is "custom") + reply_to : optional dictionary containing reply information """ - replyTo: Optional[dict[str, Any]] = None - bindingVersion: str = "custom" + reply_to: Optional[dict[str, Any]] = None diff --git a/faststream/specification/schema/channel.py b/faststream/specification/schema/channel.py deleted file mode 100644 index 89db7d7a6f..0000000000 --- a/faststream/specification/schema/channel.py +++ /dev/null @@ -1,24 +0,0 @@ -from dataclasses import dataclass -from typing import Optional - -from faststream.specification.schema.bindings import ChannelBinding -from faststream.specification.schema.operation import Operation - - -@dataclass -class Channel: - """Channel specification. - - Attributes: - description : optional description of the channel - servers : optional list of servers associated with the channel - bindings : optional channel binding - subscribe : optional operation for subscribing to the channel - publish : optional operation for publishing to the channel - """ - - description: Optional[str] = None - servers: Optional[list[str]] = None - bindings: Optional[ChannelBinding] = None - subscribe: Optional[Operation] = None - publish: Optional[Operation] = None diff --git a/faststream/specification/schema/components.py b/faststream/specification/schema/components.py deleted file mode 100644 index 39e6011591..0000000000 --- a/faststream/specification/schema/components.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import ( - Any, - Optional, -) - -from pydantic import BaseModel - -from faststream._internal._compat import ( - PYDANTIC_V2, -) -from faststream.specification.schema.message import Message - - -class Components(BaseModel): - """A class to represent components in a system. - - Attributes: - messages : Optional dictionary of messages - schemas : Optional dictionary of schemas - - Note: - The following attributes are not implemented yet: - - servers - - serverVariables - - channels - - securitySchemes - """ - - messages: Optional[dict[str, Message]] = None - schemas: Optional[dict[str, dict[str, Any]]] = None - securitySchemes: Optional[dict[str, dict[str, Any]]] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" diff --git a/faststream/specification/schema/contact.py b/faststream/specification/schema/contact.py deleted file mode 100644 index 2de5d06292..0000000000 --- a/faststream/specification/schema/contact.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import ( - Optional, -) - -from pydantic import AnyHttpUrl, BaseModel -from typing_extensions import Required, TypedDict - -from faststream._internal._compat import PYDANTIC_V2, EmailStr - - -class ContactDict(TypedDict, total=False): - """A class to represent a dictionary of contact information. - - Attributes: - name : required name of the contact (type: str) - url : URL of the contact (type: AnyHttpUrl) - email : email address of the contact (type: EmailStr) - """ - - name: Required[str] - url: AnyHttpUrl - email: EmailStr - - -class Contact(BaseModel): - """A class to represent a contact. - - Attributes: - name : name of the contact (str) - url : URL of the contact (Optional[AnyHttpUrl]) - email : email of the contact (Optional[EmailStr]) - """ - - name: str - url: Optional[AnyHttpUrl] = None - email: Optional[EmailStr] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" diff --git a/faststream/specification/schema/docs.py b/faststream/specification/schema/docs.py deleted file mode 100644 index d5b69fe7b4..0000000000 --- a/faststream/specification/schema/docs.py +++ /dev/null @@ -1,29 +0,0 @@ -from dataclasses import dataclass -from typing import Optional - -from typing_extensions import Required, TypedDict - - -class ExternalDocsDict(TypedDict, total=False): - """A dictionary type for representing external documentation. - - Attributes: - url : Required URL for the external documentation - description : Description of the external documentation - """ - - url: Required[str] - description: str - - -@dataclass -class ExternalDocs: - """A class to represent external documentation. - - Attributes: - url : URL of the external documentation - description : optional description of the external documentation - """ - - url: str - description: Optional[str] = None diff --git a/faststream/specification/schema/extra/__init__.py b/faststream/specification/schema/extra/__init__.py new file mode 100644 index 0000000000..f2417a905f --- /dev/null +++ b/faststream/specification/schema/extra/__init__.py @@ -0,0 +1,15 @@ +from .contact import Contact, ContactDict +from .external_docs import ExternalDocs, ExternalDocsDict +from .license import License, LicenseDict +from .tag import Tag, TagDict + +__all__ = ( + "Contact", + "ContactDict", + "ExternalDocs", + "ExternalDocsDict", + "License", + "LicenseDict", + "Tag", + "TagDict", +) diff --git a/faststream/specification/schema/extra/contact.py b/faststream/specification/schema/extra/contact.py new file mode 100644 index 0000000000..dfabbbacb3 --- /dev/null +++ b/faststream/specification/schema/extra/contact.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass +from typing import Optional + +from pydantic import AnyHttpUrl +from typing_extensions import Required, TypedDict + +from faststream._internal._compat import EmailStr + + +class ContactDict(TypedDict, total=False): + name: Required[str] + url: AnyHttpUrl + email: EmailStr + + +@dataclass +class Contact: + name: str + url: Optional[AnyHttpUrl] = None + email: Optional[EmailStr] = None diff --git a/faststream/specification/schema/extra/external_docs.py b/faststream/specification/schema/extra/external_docs.py new file mode 100644 index 0000000000..600a6d3a95 --- /dev/null +++ b/faststream/specification/schema/extra/external_docs.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from typing import Optional + +from typing_extensions import Required, TypedDict + + +class ExternalDocsDict(TypedDict, total=False): + url: Required[str] + description: str + + +@dataclass +class ExternalDocs: + url: str + description: Optional[str] = None diff --git a/faststream/specification/schema/extra/license.py b/faststream/specification/schema/extra/license.py new file mode 100644 index 0000000000..7bd4039621 --- /dev/null +++ b/faststream/specification/schema/extra/license.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import Optional + +from pydantic import AnyHttpUrl +from typing_extensions import Required, TypedDict + + +class LicenseDict(TypedDict, total=False): + name: Required[str] + url: AnyHttpUrl + + +@dataclass +class License: + name: str + url: Optional[AnyHttpUrl] = None diff --git a/faststream/specification/schema/extra/tag.py b/faststream/specification/schema/extra/tag.py new file mode 100644 index 0000000000..1d62ed7491 --- /dev/null +++ b/faststream/specification/schema/extra/tag.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass +from typing import Optional, Union + +from typing_extensions import Required, TypedDict + +from .external_docs import ExternalDocs, ExternalDocsDict + + +class TagDict(TypedDict, total=False): + name: Required[str] + description: str + external_docs: Union[ExternalDocs, ExternalDocsDict] + + +@dataclass +class Tag: + name: str + description: Optional[str] = None + external_docs: Optional[Union[ExternalDocs, ExternalDocsDict]] = None diff --git a/faststream/specification/schema/info.py b/faststream/specification/schema/info.py deleted file mode 100644 index 67f2341e4f..0000000000 --- a/faststream/specification/schema/info.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import ( - Any, - Optional, - Union, -) - -from pydantic import AnyHttpUrl, BaseModel - -from faststream.specification.schema.contact import Contact, ContactDict -from faststream.specification.schema.license import License, LicenseDict - - -class Info(BaseModel): - """A class to represent information. - - Attributes: - title : title of the information - version : version of the information (default: "1.0.0") - description : description of the information (default: "") - termsOfService : terms of service for the information (default: None) - contact : contact information for the information (default: None) - license : license information for the information (default: None) - """ - - termsOfService: Optional[AnyHttpUrl] = None - contact: Optional[Union[Contact, ContactDict, dict[str, Any]]] = None - license: Optional[Union[License, LicenseDict, dict[str, Any]]] = None diff --git a/faststream/specification/schema/license.py b/faststream/specification/schema/license.py deleted file mode 100644 index f95faf3e10..0000000000 --- a/faststream/specification/schema/license.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import ( - Optional, -) - -from pydantic import AnyHttpUrl, BaseModel -from typing_extensions import Required, TypedDict - -from faststream._internal._compat import ( - PYDANTIC_V2, -) - - -class LicenseDict(TypedDict, total=False): - """A dictionary-like class to represent a license. - - Attributes: - name : required name of the license (type: str) - url : URL of the license (type: AnyHttpUrl) - """ - - name: Required[str] - url: AnyHttpUrl - - -class License(BaseModel): - """A class to represent a license. - - Attributes: - name : name of the license - url : URL of the license (optional) - - Config: - extra : allow additional attributes in the model (PYDANTIC_V2) - """ - - name: str - url: Optional[AnyHttpUrl] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" diff --git a/faststream/specification/schema/message.py b/faststream/specification/schema/message.py deleted file mode 100644 index 865ec95553..0000000000 --- a/faststream/specification/schema/message.py +++ /dev/null @@ -1,51 +0,0 @@ -from dataclasses import dataclass -from typing import Any, Optional, Union - -from faststream.specification.schema.docs import ExternalDocs -from faststream.specification.schema.tag import Tag - - -@dataclass -class CorrelationId: - """Correlation ID specification. - - Attributes: - description : optional description of the correlation ID - location : location of the correlation ID - - Configurations: - extra : allows extra fields in the correlation ID model - """ - - location: str - description: Optional[str] = None - - -@dataclass -class Message: - """Message specification. - - Attributes: - title : title of the message - name : name of the message - summary : summary of the message - description : description of the message - messageId : ID of the message - correlationId : correlation ID of the message - contentType : content type of the message - payload : dictionary representing the payload of the message - tags : list of tags associated with the message - externalDocs : external documentation associated with the message - """ - - payload: dict[str, Any] - title: Optional[str] = None - name: Optional[str] = None - summary: Optional[str] = None - description: Optional[str] = None - messageId: Optional[str] = None - correlationId: Optional[CorrelationId] = None - contentType: Optional[str] = None - - tags: Optional[list[Union[Tag, dict[str, Any]]]] = None - externalDocs: Optional[Union[ExternalDocs, dict[str, Any]]] = None diff --git a/faststream/specification/schema/message/__init__.py b/faststream/specification/schema/message/__init__.py new file mode 100644 index 0000000000..6221895ab5 --- /dev/null +++ b/faststream/specification/schema/message/__init__.py @@ -0,0 +1,3 @@ +from .model import Message + +__all__ = ("Message",) diff --git a/faststream/specification/schema/message/model.py b/faststream/specification/schema/message/model.py new file mode 100644 index 0000000000..8b8c37f24a --- /dev/null +++ b/faststream/specification/schema/message/model.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from typing import Optional + +from faststream._internal.basic_types import AnyDict + + +@dataclass +class Message: + payload: AnyDict # JSON Schema + + title: Optional[str] diff --git a/faststream/specification/schema/operation.py b/faststream/specification/schema/operation.py deleted file mode 100644 index e88d3c39e4..0000000000 --- a/faststream/specification/schema/operation.py +++ /dev/null @@ -1,33 +0,0 @@ -from dataclasses import dataclass -from typing import Any, Optional, Union - -from faststream.specification.schema.bindings import OperationBinding -from faststream.specification.schema.message import Message -from faststream.specification.schema.tag import Tag - - -@dataclass -class Operation: - """A class to represent an operation. - - Attributes: - operationId : ID of the operation - summary : summary of the operation - description : description of the operation - bindings : bindings of the operation - message : message of the operation - security : security details of the operation - tags : tags associated with the operation - """ - - message: Message - - operationId: Optional[str] = None - summary: Optional[str] = None - description: Optional[str] = None - - bindings: Optional[OperationBinding] = None - - security: Optional[dict[str, list[str]]] = None - - tags: Optional[list[Union[Tag, dict[str, Any]]]] = None diff --git a/faststream/specification/schema/operation/__init__.py b/faststream/specification/schema/operation/__init__.py new file mode 100644 index 0000000000..85cbafe10a --- /dev/null +++ b/faststream/specification/schema/operation/__init__.py @@ -0,0 +1,3 @@ +from .model import Operation + +__all__ = ("Operation",) diff --git a/faststream/specification/schema/operation/model.py b/faststream/specification/schema/operation/model.py new file mode 100644 index 0000000000..2e72e523e9 --- /dev/null +++ b/faststream/specification/schema/operation/model.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from typing import Optional + +from faststream.specification.schema.bindings import OperationBinding +from faststream.specification.schema.message import Message + + +@dataclass +class Operation: + message: Message + bindings: Optional[OperationBinding] diff --git a/faststream/specification/schema/publisher.py b/faststream/specification/schema/publisher.py new file mode 100644 index 0000000000..f534619d0f --- /dev/null +++ b/faststream/specification/schema/publisher.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Optional + +from .bindings import ChannelBinding +from .operation import Operation + + +@dataclass +class PublisherSpec: + description: str + operation: Operation + bindings: Optional[ChannelBinding] diff --git a/faststream/specification/schema/security.py b/faststream/specification/schema/security.py deleted file mode 100644 index d940cbdc4f..0000000000 --- a/faststream/specification/schema/security.py +++ /dev/null @@ -1,105 +0,0 @@ -from typing import Literal, Optional - -from pydantic import AnyHttpUrl, BaseModel, Field - -from faststream._internal._compat import PYDANTIC_V2 - - -class OauthFlowObj(BaseModel): - """A class to represent an OAuth flow object. - - Attributes: - authorizationUrl : Optional[AnyHttpUrl] : The URL for authorization - tokenUrl : Optional[AnyHttpUrl] : The URL for token - refreshUrl : Optional[AnyHttpUrl] : The URL for refresh - scopes : dict[str, str] : The scopes for the OAuth flow - """ - - authorizationUrl: Optional[AnyHttpUrl] = None - tokenUrl: Optional[AnyHttpUrl] = None - refreshUrl: Optional[AnyHttpUrl] = None - scopes: dict[str, str] - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" - - -class OauthFlows(BaseModel): - """A class to represent OAuth flows. - - Attributes: - implicit : Optional[OauthFlowObj] : Implicit OAuth flow object - password : Optional[OauthFlowObj] : Password OAuth flow object - clientCredentials : Optional[OauthFlowObj] : Client credentials OAuth flow object - authorizationCode : Optional[OauthFlowObj] : Authorization code OAuth flow object - """ - - implicit: Optional[OauthFlowObj] = None - password: Optional[OauthFlowObj] = None - clientCredentials: Optional[OauthFlowObj] = None - authorizationCode: Optional[OauthFlowObj] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" - - -class SecuritySchemaComponent(BaseModel): - """A class to represent a security schema component. - - Attributes: - type : Literal, the type of the security schema component - name : optional name of the security schema component - description : optional description of the security schema component - in_ : optional location of the security schema component - schema_ : optional schema of the security schema component - bearerFormat : optional bearer format of the security schema component - openIdConnectUrl : optional OpenID Connect URL of the security schema component - flows : optional OAuth flows of the security schema component - """ - - type: Literal[ - "userPassword", - "apikey", - "X509", - "symmetricEncryption", - "asymmetricEncryption", - "httpApiKey", - "http", - "oauth2", - "openIdConnect", - "plain", - "scramSha256", - "scramSha512", - "gssapi", - ] - name: Optional[str] = None - description: Optional[str] = None - in_: Optional[str] = Field( - default=None, - alias="in", - ) - schema_: Optional[str] = Field( - default=None, - alias="schema", - ) - bearerFormat: Optional[str] = None - openIdConnectUrl: Optional[str] = None - flows: Optional[OauthFlows] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" diff --git a/faststream/specification/schema/servers.py b/faststream/specification/schema/servers.py deleted file mode 100644 index d296c359a2..0000000000 --- a/faststream/specification/schema/servers.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import Any, Optional, Union - -from pydantic import BaseModel - -from faststream._internal._compat import PYDANTIC_V2 -from faststream.specification.schema.tag import Tag - -SecurityRequirement = list[dict[str, list[str]]] - - -class ServerVariable(BaseModel): - """A class to represent a server variable. - - Attributes: - enum : list of possible values for the server variable (optional) - default : default value for the server variable (optional) - description : description of the server variable (optional) - examples : list of example values for the server variable (optional) - """ - - enum: Optional[list[str]] = None - default: Optional[str] = None - description: Optional[str] = None - examples: Optional[list[str]] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" - - -class Server(BaseModel): - """A class to represent a server. - - Attributes: - url : URL of the server - protocol : protocol used by the server - description : optional description of the server - protocolVersion : optional version of the protocol used by the server - tags : optional list of tags associated with the server - security : optional security requirement for the server - variables : optional dictionary of server variables - - Note: - The attributes `description`, `protocolVersion`, `tags`, `security`, `variables`, and `bindings` are all optional. - """ - - url: str - protocol: str - description: Optional[str] = None - protocolVersion: Optional[str] = None - tags: Optional[list[Union[Tag, dict[str, Any]]]] = None - security: Optional[SecurityRequirement] = None - variables: Optional[dict[str, ServerVariable]] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" diff --git a/faststream/specification/schema/subscriber.py b/faststream/specification/schema/subscriber.py new file mode 100644 index 0000000000..befcaf61e6 --- /dev/null +++ b/faststream/specification/schema/subscriber.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Optional + +from .bindings import ChannelBinding +from .operation import Operation + + +@dataclass +class SubscriberSpec: + description: str + operation: Operation + bindings: Optional[ChannelBinding] diff --git a/faststream/specification/schema/tag.py b/faststream/specification/schema/tag.py deleted file mode 100644 index ff9509d2c8..0000000000 --- a/faststream/specification/schema/tag.py +++ /dev/null @@ -1,37 +0,0 @@ -from dataclasses import dataclass -from typing import Optional, Union - -from typing_extensions import Required, TypedDict - -from faststream.specification.schema.docs import ExternalDocs, ExternalDocsDict - - -class TagDict(TypedDict, total=False): - """A dictionary-like class for storing tags. - - Attributes: - name : required name of the tag - description : description of the tag - externalDocs : external documentation for the tag - - """ - - name: Required[str] - description: str - externalDocs: Union[ExternalDocs, ExternalDocsDict] - - -@dataclass -class Tag: - """A class to represent a tag. - - Attributes: - name : name of the tag - description : description of the tag (optional) - externalDocs : external documentation for the tag (optional) - - """ - - name: str - description: Optional[str] = None - externalDocs: Optional[Union[ExternalDocs, ExternalDocsDict]] = None diff --git a/ruff.toml b/ruff.toml index 5cf4479d4e..f3ad63fd76 100644 --- a/ruff.toml +++ b/ruff.toml @@ -98,6 +98,12 @@ ignore = [ "N815", # Variable `*` in class scope should not be mixedCase ] +# FIXME +# "faststream/specification/asyncapi/**/*.py" = [ +# "ERA001", +# "N815", # Variable `*` in class scope should not be mixedCase +# ] + "**/fastapi/**/*.py" = [ "N803", # Argument name `expandMessageExamples` should be lowercase ] diff --git a/serve.py b/serve.py new file mode 100644 index 0000000000..bc0a693167 --- /dev/null +++ b/serve.py @@ -0,0 +1,9 @@ +from faststream import FastStream +from faststream.rabbit import RabbitBroker + +broker = RabbitBroker() +app = FastStream(broker) + +@app.after_startup +async def _(): + raise ValueError diff --git a/tests/asyncapi/base/v2_6_0/arguments.py b/tests/asyncapi/base/v2_6_0/arguments.py index b2479fc7ab..aaac817e85 100644 --- a/tests/asyncapi/base/v2_6_0/arguments.py +++ b/tests/asyncapi/base/v2_6_0/arguments.py @@ -5,7 +5,6 @@ import pydantic from dirty_equals import IsDict, IsPartialDict, IsStr from fast_depends import Depends -from fastapi import Depends as APIDepends from typing_extensions import Literal from faststream import Context @@ -17,7 +16,7 @@ class FastAPICompatible: broker_class: type[BrokerUsecase] - dependency_builder = staticmethod(APIDepends) + dependency_builder = staticmethod(Depends) def build_app(self, broker: BrokerUsecase[Any, Any]) -> BrokerUsecase[Any, Any]: """Patch it to test FastAPI scheme generation too.""" @@ -67,7 +66,7 @@ async def handle(msg) -> None: assert key == "custom_name" assert schema["channels"][key]["description"] == "Test description.", schema[ "channels" - ][key]["description"] + ][key] def test_empty(self) -> None: broker = self.broker_class() @@ -424,7 +423,7 @@ class TestModel(pydantic.BaseModel): @broker.subscriber("test") async def handle(model: TestModel) -> None: ... - schema = AsyncAPI(self.build_app(broker)).to_jsonable() + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() payload = schema["components"]["schemas"] diff --git a/tests/asyncapi/base/v2_6_0/fastapi.py b/tests/asyncapi/base/v2_6_0/fastapi.py index d814b9df4e..c6b1bd6a2d 100644 --- a/tests/asyncapi/base/v2_6_0/fastapi.py +++ b/tests/asyncapi/base/v2_6_0/fastapi.py @@ -2,7 +2,7 @@ import pytest from dirty_equals import IsStr -from fastapi import FastAPI +from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from faststream._internal.broker.broker import BrokerUsecase @@ -15,6 +15,8 @@ class FastAPITestCase: router_class: type[StreamRouter[MsgType]] broker_wrapper: Callable[[BrokerUsecase[MsgType, Any]], BrokerUsecase[MsgType, Any]] + dependency_builder = staticmethod(Depends) + @pytest.mark.skip() @pytest.mark.asyncio() async def test_fastapi_full_information(self) -> None: diff --git a/tests/asyncapi/base/v2_6_0/from_spec/__init__.py b/tests/asyncapi/base/v2_6_0/from_spec/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/asyncapi/base/v2_6_0/from_spec/test_contact.py b/tests/asyncapi/base/v2_6_0/from_spec/test_contact.py new file mode 100644 index 0000000000..ad97d872ce --- /dev/null +++ b/tests/asyncapi/base/v2_6_0/from_spec/test_contact.py @@ -0,0 +1,59 @@ +from typing import Any + +import pytest + +from faststream.specification import Contact +from faststream.specification.asyncapi.v2_6_0.schema import Contact as AsyncAPIContact + + +@pytest.mark.parametrize( + ("arg", "result"), + ( + pytest.param( + None, + None, + id="None", + ), + pytest.param( + Contact( + name="test", + url="http://contact.com", + email="support@gmail.com", + ), + AsyncAPIContact( + name="test", + url="http://contact.com", + email="support@gmail.com", + ), + id="Contact object", + ), + pytest.param( + { + "name": "test", + "url": "http://contact.com", + }, + AsyncAPIContact( + name="test", + url="http://contact.com", + ), + id="Contact dict", + ), + pytest.param( + { + "name": "test", + "url": "http://contact.com", + "email": "support@gmail.com", + "extra": "test", + }, + { + "name": "test", + "url": "http://contact.com", + "email": "support@gmail.com", + "extra": "test", + }, + id="Unknown dict", + ), + ), +) +def test_contact_factory_method(arg: Any, result: Any) -> None: + assert AsyncAPIContact.from_spec(arg) == result diff --git a/tests/asyncapi/base/v2_6_0/from_spec/test_external_docs.py b/tests/asyncapi/base/v2_6_0/from_spec/test_external_docs.py new file mode 100644 index 0000000000..7b2ede38c8 --- /dev/null +++ b/tests/asyncapi/base/v2_6_0/from_spec/test_external_docs.py @@ -0,0 +1,35 @@ +from typing import Any + +import pytest + +from faststream.specification import ExternalDocs +from faststream.specification.asyncapi.v2_6_0.schema import ExternalDocs as AsyncAPIDocs + + +@pytest.mark.parametrize( + ("arg", "result"), + ( + pytest.param( + None, + None, + id="None", + ), + pytest.param( + ExternalDocs(description="test", url="http://docs.com"), + AsyncAPIDocs(description="test", url="http://docs.com"), + id="ExternalDocs object", + ), + pytest.param( + {"description": "test", "url": "http://docs.com"}, + AsyncAPIDocs(description="test", url="http://docs.com"), + id="ExternalDocs dict", + ), + pytest.param( + {"description": "test", "url": "http://docs.com", "extra": "test"}, + {"description": "test", "url": "http://docs.com", "extra": "test"}, + id="Unknown dict", + ), + ), +) +def test_external_docs_factory_method(arg: Any, result: Any) -> None: + assert AsyncAPIDocs.from_spec(arg) == result diff --git a/tests/asyncapi/base/v2_6_0/from_spec/test_license.py b/tests/asyncapi/base/v2_6_0/from_spec/test_license.py new file mode 100644 index 0000000000..c6e2e9421b --- /dev/null +++ b/tests/asyncapi/base/v2_6_0/from_spec/test_license.py @@ -0,0 +1,35 @@ +from typing import Any + +import pytest + +from faststream.specification import License +from faststream.specification.asyncapi.v2_6_0.schema import License as AsyncAPICLicense + + +@pytest.mark.parametrize( + ("arg", "result"), + ( + pytest.param( + None, + None, + id="None", + ), + pytest.param( + License(name="test", url="http://license.com"), + AsyncAPICLicense(name="test", url="http://license.com"), + id="License object", + ), + pytest.param( + {"name": "test", "url": "http://license.com"}, + AsyncAPICLicense(name="test", url="http://license.com"), + id="License dict", + ), + pytest.param( + {"name": "test", "url": "http://license.com", "extra": "test"}, + {"name": "test", "url": "http://license.com", "extra": "test"}, + id="Unknown dict", + ), + ), +) +def test_license_factory_method(arg: Any, result: Any) -> None: + assert AsyncAPICLicense.from_spec(arg) == result diff --git a/tests/asyncapi/base/v2_6_0/from_spec/test_tag.py b/tests/asyncapi/base/v2_6_0/from_spec/test_tag.py new file mode 100644 index 0000000000..66eedcd811 --- /dev/null +++ b/tests/asyncapi/base/v2_6_0/from_spec/test_tag.py @@ -0,0 +1,49 @@ +from typing import Any + +import pytest + +from faststream.specification import ExternalDocs, Tag +from faststream.specification.asyncapi.v2_6_0.schema import ( + ExternalDocs as AsyncAPIDocs, + Tag as AsyncAPITag, +) + + +@pytest.mark.parametrize( + ("arg", "result"), + ( + pytest.param( + Tag( + name="test", + description="test", + external_docs=ExternalDocs(url="http://docs.com"), + ), + AsyncAPITag( + name="test", + description="test", + externalDocs=AsyncAPIDocs(url="http://docs.com"), + ), + id="Tag object", + ), + pytest.param( + { + "name": "test", + "description": "test", + "external_docs": {"url": "http://docs.com"}, + }, + AsyncAPITag( + name="test", + description="test", + externalDocs=AsyncAPIDocs(url="http://docs.com"), + ), + id="Tag dict", + ), + pytest.param( + {"name": "test", "description": "test", "extra": "test"}, + {"name": "test", "description": "test", "extra": "test"}, + id="Unknown dict", + ), + ), +) +def test_tag_factory_method(arg: Any, result: Any) -> None: + assert AsyncAPITag.from_spec(arg) == result diff --git a/tests/asyncapi/base/v2_6_0/publisher.py b/tests/asyncapi/base/v2_6_0/publisher.py index 6705975a72..c36cb16ecc 100644 --- a/tests/asyncapi/base/v2_6_0/publisher.py +++ b/tests/asyncapi/base/v2_6_0/publisher.py @@ -120,7 +120,7 @@ def test_not_include(self) -> None: async def handler(msg: str) -> None: pass - schema = AsyncAPI(self.build_app(broker)) + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0") assert schema.to_jsonable()["channels"] == {}, schema.to_jsonable()["channels"] @@ -133,7 +133,7 @@ class TestModel(pydantic.BaseModel): @broker.publisher("test") async def handle(msg) -> TestModel: ... - schema = AsyncAPI(self.build_app(broker)).to_jsonable() + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() payload = schema["components"]["schemas"] diff --git a/tests/asyncapi/base/v3_0_0/fastapi.py b/tests/asyncapi/base/v3_0_0/fastapi.py index fcbae28d1c..9920514b86 100644 --- a/tests/asyncapi/base/v3_0_0/fastapi.py +++ b/tests/asyncapi/base/v3_0_0/fastapi.py @@ -26,7 +26,6 @@ async def test_fastapi_full_information(self) -> None: ) app = FastAPI( - lifespan=broker.lifespan_context, title="CustomApp", version="1.1.1", description="Test description", @@ -77,7 +76,7 @@ async def test_fastapi_asyncapi_routes(self) -> None: @router.subscriber("test") async def handler() -> None: ... - app = FastAPI(lifespan=router.lifespan_context) + app = FastAPI() app.include_router(router) async with self.broker_wrapper(router.broker): @@ -107,7 +106,7 @@ async def handler() -> None: ... async def test_fastapi_asyncapi_not_fount(self) -> None: broker = self.router_factory(include_in_schema=False) - app = FastAPI(lifespan=broker.lifespan_context) + app = FastAPI() app.include_router(broker) async with self.broker_wrapper(broker.broker): @@ -125,7 +124,7 @@ async def test_fastapi_asyncapi_not_fount(self) -> None: async def test_fastapi_asyncapi_not_fount_by_url(self) -> None: broker = self.router_factory(schema_url=None) - app = FastAPI(lifespan=broker.lifespan_context) + app = FastAPI() app.include_router(broker) async with self.broker_wrapper(broker.broker): diff --git a/tests/asyncapi/confluent/v2_6_0/test_connection.py b/tests/asyncapi/confluent/v2_6_0/test_connection.py index 56ad2af682..368bbc00dd 100644 --- a/tests/asyncapi/confluent/v2_6_0/test_connection.py +++ b/tests/asyncapi/confluent/v2_6_0/test_connection.py @@ -1,6 +1,6 @@ from faststream.confluent import KafkaBroker +from faststream.specification import Tag from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.tag import Tag def test_base() -> None: diff --git a/tests/asyncapi/confluent/v3_0_0/test_connection.py b/tests/asyncapi/confluent/v3_0_0/test_connection.py index d49503ef9a..63b9c51da3 100644 --- a/tests/asyncapi/confluent/v3_0_0/test_connection.py +++ b/tests/asyncapi/confluent/v3_0_0/test_connection.py @@ -1,6 +1,6 @@ from faststream.confluent import KafkaBroker +from faststream.specification import Tag from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.tag import Tag def test_base() -> None: diff --git a/tests/asyncapi/kafka/v2_6_0/test_app.py b/tests/asyncapi/kafka/v2_6_0/test_app.py index 77470ef1cd..2bd9b5a916 100644 --- a/tests/asyncapi/kafka/v2_6_0/test_app.py +++ b/tests/asyncapi/kafka/v2_6_0/test_app.py @@ -1,9 +1,6 @@ from faststream.kafka import KafkaBroker +from faststream.specification import Contact, ExternalDocs, License, Tag from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.contact import Contact -from faststream.specification.schema.docs import ExternalDocs -from faststream.specification.schema.license import License -from faststream.specification.schema.tag import Tag def test_base() -> None: diff --git a/tests/asyncapi/kafka/v2_6_0/test_connection.py b/tests/asyncapi/kafka/v2_6_0/test_connection.py index cc7b61114b..2107e3882b 100644 --- a/tests/asyncapi/kafka/v2_6_0/test_connection.py +++ b/tests/asyncapi/kafka/v2_6_0/test_connection.py @@ -1,6 +1,6 @@ from faststream.kafka import KafkaBroker +from faststream.specification import Tag from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.tag import Tag def test_base() -> None: diff --git a/tests/asyncapi/kafka/v3_0_0/test_connection.py b/tests/asyncapi/kafka/v3_0_0/test_connection.py index 280cb798d1..e1fb6cfaab 100644 --- a/tests/asyncapi/kafka/v3_0_0/test_connection.py +++ b/tests/asyncapi/kafka/v3_0_0/test_connection.py @@ -1,6 +1,6 @@ from faststream.kafka import KafkaBroker +from faststream.specification import Tag from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.tag import Tag def test_base() -> None: diff --git a/tests/asyncapi/nats/v2_6_0/test_arguments.py b/tests/asyncapi/nats/v2_6_0/test_arguments.py index d3b9b53a34..5ad34a0001 100644 --- a/tests/asyncapi/nats/v2_6_0/test_arguments.py +++ b/tests/asyncapi/nats/v2_6_0/test_arguments.py @@ -17,4 +17,4 @@ async def handle(msg) -> None: ... assert schema["channels"][key]["bindings"] == { "nats": {"bindingVersion": "custom", "subject": "test"}, - } + }, schema["channels"][key]["bindings"] diff --git a/tests/asyncapi/nats/v2_6_0/test_connection.py b/tests/asyncapi/nats/v2_6_0/test_connection.py index 8cb4110d78..486bbb8033 100644 --- a/tests/asyncapi/nats/v2_6_0/test_connection.py +++ b/tests/asyncapi/nats/v2_6_0/test_connection.py @@ -1,6 +1,6 @@ from faststream.nats import NatsBroker +from faststream.specification import Tag from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.tag import Tag def test_base() -> None: diff --git a/tests/asyncapi/nats/v3_0_0/test_connection.py b/tests/asyncapi/nats/v3_0_0/test_connection.py index f4913252ef..f88fc0fb83 100644 --- a/tests/asyncapi/nats/v3_0_0/test_connection.py +++ b/tests/asyncapi/nats/v3_0_0/test_connection.py @@ -1,6 +1,6 @@ from faststream.nats import NatsBroker +from faststream.specification import Tag from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.tag import Tag def test_base() -> None: diff --git a/tests/asyncapi/rabbit/v2_6_0/test_connection.py b/tests/asyncapi/rabbit/v2_6_0/test_connection.py index 6fae2ac389..be4d7a7edc 100644 --- a/tests/asyncapi/rabbit/v2_6_0/test_connection.py +++ b/tests/asyncapi/rabbit/v2_6_0/test_connection.py @@ -1,6 +1,6 @@ from faststream.rabbit import RabbitBroker +from faststream.specification import Tag from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.tag import Tag def test_base() -> None: @@ -115,4 +115,4 @@ def test_custom() -> None: }, }, } - ) + ), schema diff --git a/tests/asyncapi/rabbit/v2_6_0/test_publisher.py b/tests/asyncapi/rabbit/v2_6_0/test_publisher.py index b9c17a9d00..abe3255a3d 100644 --- a/tests/asyncapi/rabbit/v2_6_0/test_publisher.py +++ b/tests/asyncapi/rabbit/v2_6_0/test_publisher.py @@ -106,6 +106,14 @@ async def handle(msg) -> None: ... }, }, "publish": { + "bindings": { + "amqp": { + "ack": True, + "bindingVersion": "0.2.0", + "deliveryMode": 1, + "mandatory": True, + }, + }, "message": { "$ref": "#/components/messages/_:test-ex:Publisher:Message", }, diff --git a/tests/asyncapi/rabbit/v3_0_0/test_connection.py b/tests/asyncapi/rabbit/v3_0_0/test_connection.py index 0403cef9c5..971a89afec 100644 --- a/tests/asyncapi/rabbit/v3_0_0/test_connection.py +++ b/tests/asyncapi/rabbit/v3_0_0/test_connection.py @@ -1,6 +1,6 @@ from faststream.rabbit import RabbitBroker +from faststream.specification import Tag from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.tag import Tag def test_base() -> None: diff --git a/tests/asyncapi/rabbit/v3_0_0/test_publisher.py b/tests/asyncapi/rabbit/v3_0_0/test_publisher.py index 1456b5b86d..a108270da5 100644 --- a/tests/asyncapi/rabbit/v3_0_0/test_publisher.py +++ b/tests/asyncapi/rabbit/v3_0_0/test_publisher.py @@ -141,13 +141,19 @@ async def handle(msg) -> None: ... assert schema["operations"] == { "_:test-ex:Publisher": { "action": "send", - "channel": { - "$ref": "#/channels/_:test-ex:Publisher", + "bindings": { + "amqp": { + "ack": True, + "bindingVersion": "0.3.0", + "deliveryMode": 1, + "mandatory": True, + } }, + "channel": {"$ref": "#/channels/_:test-ex:Publisher"}, "messages": [ - {"$ref": "#/channels/_:test-ex:Publisher/messages/Message"}, + {"$ref": "#/channels/_:test-ex:Publisher/messages/Message"} ], - }, + } } def test_reusable_exchange(self) -> None: diff --git a/tests/asyncapi/redis/v2_6_0/test_arguments.py b/tests/asyncapi/redis/v2_6_0/test_arguments.py index b65598f6f5..403cccad84 100644 --- a/tests/asyncapi/redis/v2_6_0/test_arguments.py +++ b/tests/asyncapi/redis/v2_6_0/test_arguments.py @@ -79,8 +79,8 @@ async def handle(msg) -> None: ... "redis": { "bindingVersion": "custom", "channel": "test", - "consumer_name": "consumer", - "group_name": "group", + "consumerName": "consumer", + "groupName": "group", "method": "xreadgroup", }, } diff --git a/tests/asyncapi/redis/v2_6_0/test_connection.py b/tests/asyncapi/redis/v2_6_0/test_connection.py index 221e4cd430..194371e767 100644 --- a/tests/asyncapi/redis/v2_6_0/test_connection.py +++ b/tests/asyncapi/redis/v2_6_0/test_connection.py @@ -1,6 +1,6 @@ from faststream.redis import RedisBroker +from faststream.specification import Tag from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.tag import Tag def test_base() -> None: diff --git a/tests/asyncapi/redis/v3_0_0/test_arguments.py b/tests/asyncapi/redis/v3_0_0/test_arguments.py index b9a3136274..0def5e4f41 100644 --- a/tests/asyncapi/redis/v3_0_0/test_arguments.py +++ b/tests/asyncapi/redis/v3_0_0/test_arguments.py @@ -79,8 +79,8 @@ async def handle(msg) -> None: ... "redis": { "bindingVersion": "custom", "channel": "test", - "consumer_name": "consumer", - "group_name": "group", + "consumerName": "consumer", + "groupName": "group", "method": "xreadgroup", }, } diff --git a/tests/asyncapi/redis/v3_0_0/test_connection.py b/tests/asyncapi/redis/v3_0_0/test_connection.py index 51d7224c50..968e67b464 100644 --- a/tests/asyncapi/redis/v3_0_0/test_connection.py +++ b/tests/asyncapi/redis/v3_0_0/test_connection.py @@ -1,6 +1,6 @@ from faststream.redis import RedisBroker +from faststream.specification import Tag from faststream.specification.asyncapi import AsyncAPI -from faststream.specification.schema.tag import Tag def test_base() -> None: diff --git a/tests/cli/test_asyncapi_docs.py b/tests/cli/test_asyncapi_docs.py index 9deb1877b9..344ba54013 100644 --- a/tests/cli/test_asyncapi_docs.py +++ b/tests/cli/test_asyncapi_docs.py @@ -1,5 +1,6 @@ import json import sys +import traceback from http.server import HTTPServer from pathlib import Path from unittest.mock import Mock @@ -94,7 +95,7 @@ def test_serve_asyncapi_json_schema( m.setattr(HTTPServer, "serve_forever", mock) r = runner.invoke(cli, SERVE_CMD + [str(schema_path)]) # noqa: RUF005 - assert r.exit_code == 0, r.exc_info + assert r.exit_code == 0, traceback.format_tb(r.exc_info[2]) mock.assert_called_once() schema_path.unlink() @@ -115,7 +116,7 @@ def test_serve_asyncapi_yaml_schema( m.setattr(HTTPServer, "serve_forever", mock) r = runner.invoke(cli, SERVE_CMD + [str(schema_path)]) # noqa: RUF005 - assert r.exit_code == 0, r.exc_info + assert r.exit_code == 0, traceback.format_tb(r.exc_info[2]) mock.assert_called_once() schema_path.unlink() diff --git a/tests/cli/test_run_regular.py b/tests/cli/test_run.py similarity index 100% rename from tests/cli/test_run_regular.py rename to tests/cli/test_run.py diff --git a/tests/cli/test_run_asgi.py b/tests/cli/test_run_asgi.py index c644c04bb2..49825f932b 100644 --- a/tests/cli/test_run_asgi.py +++ b/tests/cli/test_run_asgi.py @@ -34,14 +34,7 @@ def test_run_as_asgi(runner: CliRunner) -> None: assert result.exit_code == 0 -@pytest.mark.parametrize( - "workers", - ( - pytest.param(1), - pytest.param(2), - pytest.param(5), - ), -) +@pytest.mark.parametrize("workers", (pytest.param(1), pytest.param(2), pytest.param(5))) def test_run_as_asgi_with_workers(runner: CliRunner, workers: int) -> None: app = AsgiFastStream(AsyncMock()) app.run = AsyncMock()