diff --git a/.cursor/rules/faststream.mdc b/.cursor/rules/faststream.mdc new file mode 100644 index 0000000000..38134ae10c --- /dev/null +++ b/.cursor/rules/faststream.mdc @@ -0,0 +1,105 @@ +--- +description: FastStream python framework usage rules +globs: *.py +alwaysApply: false +--- + +- Always pass broker to `FastStream` object as a positional argument +- You can't use `app.broker` / `FastStream.broker` / `AsgiFastStream.broker` attribute in your code +- `app.run` / `FastStream.run` / `AsgiFastStream.run` is an async function, so it should be called by `asyncio.run` +- To log something always use `faststream.Logger` passing as a Context- Don't use `print(...)` to log anything in your code. Use `faststream.Logger` instead +- Never do create broker inside application `FastStream(Broker())` +- Subscribers have to be created by broker / router only: `@broker.subscriber` / `@router.subscriber` +- Publishers have to be created by broker / router only: `@broker.publisher` / `@router.publisher` +- Always use type annotations in `@broker.subscriber` or `@router.subscriber` decorated functions. Result annotation should be `None` by default +- Never add to `@broker.subscriber` or `@router.subscriber` decorated functions useless in code options. + +## JSON Serialization + +To serialize incoming messages with complex JSON structure use `Pydantic` models in annotations + +```python +from pydantic import BaseModel + +class IncomingMessage(BaseModel): + field: str + another_field: str + +@broker.subscriber("in") +async def handler(body: IncomingMessage) -> None: + ... +``` + +## Path feature + +To consume information from incoming subject, use `faststream.Path` + +```python +from typing import Annotated +from faststream import Path + +@broker.subscriber("logs.{log_level}") +async def handle_logs(log_level: Annotated[str, Path()]) -> None: + ... +``` + +You have to import `faststream.Path` to use this feature + +## Context + +FastStreams has its own Dependency Injection container - Context, used to store application runtime objects and variables. + +```python +from typing import Annotated +from faststream import Context, FastStream +from faststream.kafka import KafkaBroker, KafkaMessage + +broker = KafkaBroker() +app = FastStream(broker) + +@broker.subscriber("test") +async def base_handler(body: str, message: Annotated[KafkaMessage, Context()]) -> None: + ... +``` + +Context already contains some global objects that you can always access: + +* broker - the current broker +* context - the context itself, in which you can write your own fields +* logger - the logger used for your broker (tags messages with message_id) +* message - the raw message (if you need access to it) + +To use them, simply import and use them as subscriber argument annotations. + +Shared aliases + +```python +from faststream import Logger, ContextRepo, NoCast +``` + +And per-broker + +```python +from faststream.[broker] import [Broker]Message +``` + +```python +from faststream import FastStream, Context, Logger, ContextRepo +from faststream.kafka import KafkaBroker, KafkaMessage +from faststream.kafka.annotations import KafkaBroker as BrokerAnnotation + +broker_object = KafkaBroker("localhost:9092") +app = FastStream(broker_object) + +@broker_object.subscriber("response-topic") +async def handle_response( + logger: Logger, + message: KafkaMessage, + context: ContextRepo, + broker: BrokerAnnotation, +) -> None: + logger.info(message) + await broker.publish("test", "response") +``` + +Don't add useless Context options to function signature. Use only required ones.Don't import useless annotations as well. diff --git a/.devcontainer/devcontainer.env b/.devcontainer/devcontainer.env deleted file mode 100644 index 4305d46f0d..0000000000 --- a/.devcontainer/devcontainer.env +++ /dev/null @@ -1 +0,0 @@ -CONTAINER_PREFIX=${USER} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 3c5c4dcc92..0000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "python-3.12", - "dockerComposeFile": [ - "./docker-compose.yaml" - ], - "service": "python-3.12-faststream", - "shutdownAction": "stopCompose", - "workspaceFolder": "/workspaces/faststream", - // "runArgs": [], - "remoteEnv": {}, - "features": { - "ghcr.io/devcontainers/features/common-utils:2": { - "installZsh": true, - "installOhMyZsh": true, - "configureZshAsDefaultShell": true, - "username": "vscode", - "userUid": "1000", - "userGid": "1000" - // "upgradePackages": "true" - }, - "ghcr.io/devcontainers/features/git:1": {}, - "ghcr.io/devcontainers/features/git-lfs:1": {}, - "ghcr.io/devcontainers/features/python:1": { - "version": "3.12" // python 3.12 is installed here - } - }, - "updateContentCommand": "bash .devcontainer/setup.sh", - "postCreateCommand": [], - "customizations": { - "vscode": { - "settings": { - "python.linting.enabled": true, - "python.testing.pytestEnabled": true, - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "always" - }, - "[python]": { - "editor.defaultFormatter": "ms-python.vscode-pylance" - }, - "editor.rulers": [ - 80 - ] - }, - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance" - ] - } - } -} diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml deleted file mode 100644 index 211bac2e7a..0000000000 --- a/.devcontainer/docker-compose.yaml +++ /dev/null @@ -1,88 +0,0 @@ -version: '3' - -services: - python-3.12-faststream: # nosemgrep - # Installing python 3.10 here which is needed for pre-commit - # Actual python 3.12 is installed in devcontainer.json - image: mcr.microsoft.com/devcontainers/python:3.10 - container_name: python-3.12-faststream - volumes: - - ../:/workspaces/faststream:cached - command: sleep infinity - env_file: - - ./devcontainer.env - networks: - - faststream-network - extra_hosts: - - "localhost:host-gateway" - - # nosemgrep: yaml.docker-compose.security.writable-filesystem-service.writable-filesystem-service - rabbitmq-faststream: - image: rabbitmq:alpine - container_name: faststream-${USER}-rabbitmq-py312 - env_file: - - ./devcontainer.env - ports: - - "5672:5672" - # https://semgrep.dev/r?q=yaml.docker-compose.security.no-new-privileges.no-new-privileges - security_opt: - - no-new-privileges:true - networks: - - faststream-network - # nosemgrep: yaml.docker-compose.security.writable-filesystem-service.writable-filesystem-service - kafka-faststream: - image: bitnami/kafka:3.5.0 - container_name: faststream-${USER}-kafka-py312 - env_file: - - ./devcontainer.env - ports: - - "9092:9092" - - "9093:9093" - environment: - KAFKA_ENABLE_KRAFT: "true" - KAFKA_CFG_NODE_ID: "1" - KAFKA_CFG_PROCESS_ROLES: "broker,controller" - KAFKA_CFG_CONTROLLER_LISTENER_NAMES: "CONTROLLER" - KAFKA_CFG_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093" - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" - KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9092" - KAFKA_BROKER_ID: "1" - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "1@localhost:9093" - ALLOW_PLAINTEXT_LISTENER: "true" - # https://semgrep.dev/r?q=yaml.docker-compose.security.no-new-privileges.no-new-privileges - security_opt: - - no-new-privileges:true - networks: - - faststream-network - # nosemgrep: yaml.docker-compose.security.writable-filesystem-service.writable-filesystem-service - nats-faststream: - image: nats - container_name: faststream-${USER}-nats-py312 - command: -js - env_file: - - ./devcontainer.env - ports: - - 4222:4222 - - 8222:8222 # management - # https://semgrep.dev/r?q=yaml.docker-compose.security.no-new-privileges.no-new-privileges - security_opt: - - no-new-privileges:true - networks: - - faststream-network - # nosemgrep: yaml.docker-compose.security.writable-filesystem-service.writable-filesystem-service - redis-faststream: - image: redis:alpine - container_name: faststream-${USER}-redis-py312 - env_file: - - ./devcontainer.env - ports: - - 6379:6379 - # https://semgrep.dev/r?q=yaml.docker-compose.security.no-new-privileges.no-new-privileges - security_opt: - - no-new-privileges:true - networks: - - faststream-network - -networks: - faststream-network: - name: "faststream-${USER}-network" diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh deleted file mode 100644 index 3630063d7f..0000000000 --- a/.devcontainer/setup.sh +++ /dev/null @@ -1,8 +0,0 @@ -# Update pip -pip install --upgrade pip - -# Install dev packages -pip install -e ".[dev]" - -# Install pre-commit hooks if not installed already -pre-commit install diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/SECURITY.md b/.github/SECURITY.md similarity index 100% rename from SECURITY.md rename to .github/SECURITY.md diff --git a/.github/workflows/docs_deploy.yaml b/.github/workflows/docs_deploy.yaml index 7daac9b471..8d385f9051 100644 --- a/.github/workflows/docs_deploy.yaml +++ b/.github/workflows/docs_deploy.yaml @@ -28,9 +28,9 @@ jobs: path: .cache - run: | set -ux - uv pip install --system -e .[dev] + uv pip install --system --group dev -e . uv pip uninstall --system email-validator # This is to fix broken link in docs - - run: ./scripts/build-docs.sh + - run: cd docs && python docs.py build - run: echo "VERSION=$(python3 -c 'from importlib.metadata import version; print(".".join(version("faststream").split(".")[:2]))')" >> $GITHUB_ENV - run: echo "IS_RC=$(python3 -c 'from importlib.metadata import version; print("rc" in version("faststream"))')" >> $GITHUB_ENV - name: Configure Git user diff --git a/.github/workflows/pr_tests.yaml b/.github/workflows/pr_tests.yaml index 6a5f87d1a4..8412d3569c 100644 --- a/.github/workflows/pr_tests.yaml +++ b/.github/workflows/pr_tests.yaml @@ -31,6 +31,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" + - uses: extractions/setup-just@v2 - name: Set $PY environment variable run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV - uses: actions/cache@v4 @@ -44,7 +45,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] pydantic-version: ["pydantic-v1", "pydantic-v2"] fail-fast: false @@ -65,7 +66,7 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[optionals,testing] + uv pip install --system --group optionals --group testing . - name: Install Pydantic v1 if: matrix.pydantic-version == 'pydantic-v1' run: uv pip install --system "pydantic>=1.10.0,<2.0.0" @@ -75,8 +76,8 @@ jobs: - run: mkdir coverage - name: Test run: > - bash scripts/test.sh -vv - -m "(slow and (${{env.ALL_PYTEST_MARKERS}})) or (${{env.ALL_PYTEST_MARKERS}})" + coverage run -m pytest + -vv -m "(slow and (${{env.ALL_PYTEST_MARKERS}})) or (${{env.ALL_PYTEST_MARKERS}})" env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} @@ -103,11 +104,11 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[optionals,testing] + uv pip install --system --group optionals --group testing . - name: Test run: > - bash scripts/test.sh - -m "(slow and (${{env.ALL_PYTEST_MARKERS}})) or (${{env.ALL_PYTEST_MARKERS}})" + coverage run -m pytest + -vv -m "(slow and (${{env.ALL_PYTEST_MARKERS}})) or (${{env.ALL_PYTEST_MARKERS}})" test-windows-latest: if: github.event.pull_request.draft == false @@ -124,11 +125,11 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[optionals,testing] + uv pip install --system --group optionals --group testing . - name: Test run: > - bash scripts/test.sh - -m "(slow and (${{env.ALL_PYTEST_MARKERS}})) or (${{env.ALL_PYTEST_MARKERS}})" + coverage run -m pytest + -vv -m "(slow and (${{env.ALL_PYTEST_MARKERS}})) or (${{env.ALL_PYTEST_MARKERS}})" test-kafka-smoke: if: github.event.pull_request.draft == false @@ -145,11 +146,11 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[kafka,test-core,cli] + uv pip install --system --group test-core ".[cli,kafka]" - name: Test run: > - bash scripts/test.sh - -m "not kafka" + coverage run -m pytest + -vv -m "not kafka" tests/brokers/kafka/test_test_client.py test-kafka-real: @@ -186,12 +187,12 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[optionals,testing] + uv pip install --system --group optionals --group testing . - run: mkdir coverage - name: Test run: > - bash scripts/test.sh - -m "(slow and kafka) or kafka" + coverage run -m pytest + -vv -m "(slow and kafka) or kafka" env: COVERAGE_FILE: coverage/.coverage.kafka-py CONTEXT: kafka-py @@ -218,11 +219,11 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[confluent,test-core,cli] + uv pip install --system --group test-core ".[cli,confluent]" - name: Test run: > - bash scripts/test.sh - -m "not confluent" + coverage run -m pytest + -vv -m "not confluent" tests/brokers/confluent/test_test_client.py test-confluent-real: @@ -259,12 +260,12 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[optionals,testing] + uv pip install --system --group optionals --group testing . - run: mkdir coverage - name: Test run: > - bash scripts/test.sh -vv - -m "(slow and confluent) or confluent" + coverage run -m pytest + -vv -m "(slow and confluent) or confluent" env: COVERAGE_FILE: coverage/.coverage.confluent-py CONTEXT: confluent-py @@ -291,11 +292,11 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[rabbit,test-core,cli] + uv pip install --system --group test-core ".[cli,rabbit]" - name: Test run: > - bash scripts/test.sh - -m "not rabbit" + coverage run -m pytest + -vv -m "not rabbit" tests/brokers/rabbit/test_test_client.py test-rabbit-real: @@ -321,12 +322,12 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[optionals,testing] + uv pip install --system --group optionals --group testing . - run: mkdir coverage - name: Test run: > - bash scripts/test.sh - -m "(slow and rabbit) or rabbit" + coverage run -m pytest + -vv -m "(slow and rabbit) or rabbit" env: COVERAGE_FILE: coverage/.coverage.rabbit-py CONTEXT: rabbit-py @@ -353,11 +354,11 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[nats,test-core,cli] + uv pip install --system --group test-core ".[cli,nats]" - name: Test run: > - bash scripts/test.sh - -m "not nats" + coverage run -m pytest + -vv -m "not nats" tests/brokers/nats/test_test_client.py test-nats-real: @@ -383,12 +384,12 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[optionals,testing] + uv pip install --system --group optionals --group testing . - run: mkdir coverage - name: Test run: > - bash scripts/test.sh - -m "(slow and nats) or nats" + coverage run -m pytest + -vv -m "(slow and nats) or nats" env: COVERAGE_FILE: coverage/.coverage.nats-py CONTEXT: nats-py @@ -415,11 +416,11 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[redis,test-core,cli] + uv pip install --system --group test-core ".[cli,redis]" - name: Test run: > - bash scripts/test.sh - -m "not redis" + coverage run -m pytest + -vv -m "not redis" tests/brokers/redis/test_test_client.py test-redis-real: @@ -445,12 +446,12 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - uv pip install --system .[optionals,testing] + uv pip install --system --group optionals --group testing . - run: mkdir coverage - name: Test run: > - bash scripts/test.sh - -m "(slow and redis) or redis" + coverage run -m pytest + -vv -m "(slow and redis) or redis" env: COVERAGE_FILE: coverage/.coverage.redis-py CONTEXT: redis-py @@ -480,7 +481,7 @@ jobs: version: "latest" - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.12" - name: Get coverage files uses: actions/download-artifact@v4 diff --git a/.github/workflows/pr_update-linting-and-api-references.yaml b/.github/workflows/pr_update-linting-and-api-references.yaml index 6a8ed77309..2b1dc803da 100644 --- a/.github/workflows/pr_update-linting-and-api-references.yaml +++ b/.github/workflows/pr_update-linting-and-api-references.yaml @@ -38,7 +38,7 @@ jobs: # should install with `-e` run: | set -ux - uv pip install --system -e .[dev] + uv pip install --system --group dev -e . - uses: pre-commit/action@v3.0.1 continue-on-error: true @@ -46,7 +46,7 @@ jobs: extra_args: --hook-stage manual --all-files - name: Run build docs - run: bash scripts/build-docs.sh + run: cd docs && python docs.py build - name: Commit uses: stefanzweifel/git-auto-commit-action@v6 diff --git a/.github/workflows/release_pypi.yaml b/.github/workflows/release_pypi.yaml index f1ed153dde..a23ad498b8 100644 --- a/.github/workflows/release_pypi.yaml +++ b/.github/workflows/release_pypi.yaml @@ -9,6 +9,8 @@ on: jobs: publish: runs-on: ubuntu-latest + permissions: + id-token: write steps: - name: Dump GitHub context @@ -41,7 +43,6 @@ jobs: - name: Publish uses: pypa/gh-action-pypi-publish@release/v1 # nosemgrep with: - password: ${{ secrets.PYPI_API_TOKEN }} skip-existing: true - name: Dump GitHub context diff --git a/.gitignore b/.gitignore index ec095d1691..c85039ee25 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ venv* htmlcov token .DS_Store +*.egg-info docs/site/ docs/site_build/ diff --git a/CITATION.cff b/CITATION.cff index 82406c1ee9..9ca07cd7eb 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -10,7 +10,7 @@ type: software authors: - given-names: Nikita family-names: Pastukhov - email: diementros@yandex.com + email: nikita@pastukhov-dev.ru - given-names: Davor family-names: Runje email: davor@ag2.ai diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b8fb8c0a3f..0092df1c95 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,69 +4,92 @@ After cloning the project, you'll need to set up the development environment. Here are the guidelines on how to do this. -## Virtual Environment with `venv` +## Install Justfile Utility -Create a virtual environment in a directory using Python's `venv` module: +Install justfile on your system: ```bash -python -m venv venv +brew install justfile ``` -That will create a `./venv/` directory with Python binaries, allowing you to install packages in an isolated environment. +View all available commands: -## Activate the Environment +```bash +just +``` + +## Init development environment -Activate the new environment with: +Build faststream image: ```bash -source ./venv/bin/activate +just init ``` -Ensure you have the latest pip version in your virtual environment: +By default, this builds Python 3.10. If you need another version, pass it as an argument to the just command: ```bash -python -m pip install --upgrade pip +just init 3.11.5 ``` -## Installing Dependencies +To check available Python versions, refer to the pyproject.toml file in the project root. -After activating the virtual environment as described above, run: +## Run all Dependencies + +Start all dependencies as docker containers: ```bash -pip install -e ".[dev]" +just up ``` -This will install all the dependencies and your local **FastStream** in your virtual environment. +Once you are done with development and running tests, you can stop the dependencies' docker containers by running: -### Using Your local **FastStream** +```bash +just stop +# or +just down +``` -If you create a Python file that imports and uses **FastStream**, and run it with the Python from your local environment, it will use your local **FastStream** source code. +## Running Tests -Whenever you update your local **FastStream** source code, it will automatically use the latest version when you run your Python file again. This is because it is installed with `-e`. +To run fast tests, use: -This way, you don't have to "install" your local version to be able to test every change. +```bash +just test +``` -To use your local **FastStream CLI**, type: +To run all tests with brokers connections, use: ```bash -python -m faststream ... +just test-all ``` -## Running Tests +To run tests with coverage: + +```bash +just coverage-test +``` +If you need test only specific folder or broker: -### Pytest +```bash +just test tests/brokers/kafka +# or +just test-all tests/brokers/kafka +# or +just coverage-test tests/brokers/kafka +``` -To run tests with your current **FastStream** application and Python environment, use: +If you need some pytest arguments: ```bash -pytest tests +just test -vv # or -./scripts/test.sh -# with coverage output -./scripts/test-cov.sh +just test tests/brokers/kafka -vv +# or +just test "-vv -s" ``` -In your project, you'll find some *pytest marks*: +In your project, some tests are grouped under specific pytest marks: * **slow** * **rabbit** @@ -75,34 +98,64 @@ In your project, you'll find some *pytest marks*: * **redis** * **all** -By default, running *pytest* will execute "not slow" tests. - -To run all tests use: +By default, "just test" will execute "not slow and not kafka and not confluent and not redis and not rabbit and not nats" tests. +"just test-all" will execute tests with mark "all". +You can specify marks to include or exclude tests: ```bash -pytest -m 'all' +just test tests/ -vv "not kafka and not rabbit" +# or +just test . -vv "not kafka and not rabbit" +# or if you no need pytest arguments +just test . "" "not kafka and not rabbit" ``` -If you don't have a local broker instance running, you can run tests without those dependencies: +## Linter + +Run all linters: ```bash -pytest -m 'not rabbit and not kafka and not nats and not redis and not confluent' +just linter +``` +This command run ruff check, ruff format and codespell. + +To use specific command +```bash +just ruff-check +# or +just ruff-format +# or +just codespell ``` -To run tests based on RabbitMQ, Kafka, or other dependencies, the following dependencies are needed to be started as docker containers: +## Static analysis -```yaml -{! includes/docker-compose.yaml !} +Run static analysis all tools: + +```bash +just static-analysis ``` +This command run mypy, bandit and semgrep. -You can start the dependencies easily using provided script by running: +To use specific command +```bash +just mypy +# or +just bandit +# or +just semgrep +``` + +## Docs + +Build docs: ```bash -./scripts/start_test_env.sh +just docs-build ``` -Once you are done with development and running tests, you can stop the dependencies' docker containers by running: +Run docs: ```bash -./scripts/stop_test_env.sh +just docs-serve ``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..4847f9fb31 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +ARG PYTHON_VERSION=3.10 + +FROM python:$PYTHON_VERSION +COPY --from=ghcr.io/astral-sh/uv:0.7.13 /uv /uvx /bin/ + +ENV PYTHONUNBUFFERED=1 + +COPY . /src + +WORKDIR /src + +RUN uv sync --group dev diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000000..1128f9daa8 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,48 @@ +services: + rabbitmq: + image: rabbitmq:alpine + ports: + - "5672:5672" + security_opt: + - no-new-privileges:true + + kafka: + image: bitnami/kafka:3.5.0 + ports: + - "9092:9092" + environment: + KAFKA_ENABLE_KRAFT: "true" + KAFKA_CFG_NODE_ID: "1" + KAFKA_CFG_PROCESS_ROLES: "broker,controller" + KAFKA_CFG_CONTROLLER_LISTENER_NAMES: "CONTROLLER" + KAFKA_CFG_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093" + KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" + KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://127.0.0.1:9092" + KAFKA_BROKER_ID: "1" + KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093" + ALLOW_PLAINTEXT_LISTENER: "true" + security_opt: + - no-new-privileges:true + + nats: + image: nats + command: -js + ports: + - 4222:4222 + - 8222:8222 # management + security_opt: + - no-new-privileges:true + + redis: + image: redis:alpine + ports: + - 6379:6379 + security_opt: + - no-new-privileges:true + + faststream: + build: . + volumes: + - ./:/src + network_mode: "host" + tty: true diff --git a/docs/create_api_docs.py b/docs/create_api_docs.py index f536f01d15..bfe7ce9b70 100644 --- a/docs/create_api_docs.py +++ b/docs/create_api_docs.py @@ -65,7 +65,8 @@ def _get_submodules(package_name: str) -> List[str]: def _import_submodules( - module_name: str, include_public_api_only: bool = False + module_name: str, + include_public_api_only: bool = False, ) -> Optional[List[ModuleType]]: def _import_module(name: str) -> Optional[ModuleType]: try: @@ -91,7 +92,8 @@ def _import_module(name: str) -> Optional[ModuleType]: def _import_functions_and_classes( - m: ModuleType, include_public_api_only: bool = False + m: ModuleType, + include_public_api_only: bool = False, ) -> List[Tuple[str, Union[FunctionType, Type[Any]]]]: funcs_and_classes = [] if not include_public_api_only: @@ -114,20 +116,23 @@ def _is_private(name: str) -> bool: def _import_all_members( - module_name: str, include_public_api_only: bool = False + module_name: str, + include_public_api_only: bool = False, ) -> List[str]: submodules = _import_submodules( - module_name, include_public_api_only=include_public_api_only + module_name, + include_public_api_only=include_public_api_only, ) members: List[Tuple[str, Union[FunctionType, Type[Any]]]] = list( itertools.chain( *[ _import_functions_and_classes( - m, include_public_api_only=include_public_api_only + m, + include_public_api_only=include_public_api_only, ) for m in submodules - ] - ) + ], + ), ) names = [ @@ -154,7 +159,7 @@ def _f(x: str) -> List[str]: xs = x.split(".") return [".".join(xs[:i]) + "." for i in range(1, len(xs))] - def _get_sorting_key(item): + def _get_sorting_key(item: str) -> str: y = item.split(".") z = [f"~{a}" for a in y[:-1]] + [y[-1]] return ".".join(z) @@ -170,9 +175,8 @@ def _get_api_summary_item(x: str) -> str: if x.endswith("."): indent = " " * (4 * (len(xs) - 1 + 1)) return f"{indent}- {xs[-2]}" - else: - indent = " " * (4 * (len(xs) + 1)) - return f"{indent}- [{xs[-1]}](api/{'/'.join(xs)}.md)" + indent = " " * (4 * (len(xs) + 1)) + return f"{indent}- [{xs[-1]}](api/{'/'.join(xs)}.md)" def _get_api_summary(members: List[str]) -> str: @@ -228,17 +232,24 @@ def _load_submodules( """ submodules = _import_submodules(module_name) members = itertools.chain(*map(_import_functions_and_classes, submodules)) - names = [ - y - for _, y in members - if (isinstance(y, str) and y in members_with_submodules) - or (f"{y.__module__}.{y.__name__}" in members_with_submodules) - ] + + names: list[Union[FunctionType, Type[Any]]] = [] + for _, y in members: + if isinstance(y, str): + name = y + else: + name = f"{y.__module__}.{y.__name__}" + + if name in members_with_submodules: + names.append(y) + return names def _update_single_api_doc( - symbol: Union[FunctionType, Type[Any]], docs_path: Path, module_name: str + symbol: Union[FunctionType, Type[Any]], + docs_path: Path, + module_name: str, ) -> None: if isinstance(symbol, str): class_name = symbol.split(".")[-1] @@ -263,11 +274,15 @@ def _update_single_api_doc( def _update_api_docs( - symbols: List[Union[FunctionType, Type[Any]]], docs_path: Path, module_name: str + symbols: List[Union[FunctionType, Type[Any]]], + docs_path: Path, + module_name: str, ) -> None: for symbol in symbols: _update_single_api_doc( - symbol=symbol, docs_path=docs_path, module_name=module_name + symbol=symbol, + docs_path=docs_path, + module_name=module_name, ) @@ -303,6 +318,14 @@ def _generate_api_docs_for_module() -> Tuple[str, str]: _update_api_docs(symbols, API_DIR, MODULE) + # TODO: fix the problem and remove this + src = """ - [ContactDict](api/faststream/asyncapi/schema/info/ContactDict.md) +""" + dst = """ - [ContactDict](api/faststream/asyncapi/schema/info/ContactDict.md) + - [EmailStr](api/faststream/asyncapi/schema/info/EmailStr.md) +""" + api_summary = api_summary.replace(src, dst) + return " - All API\n" + api_summary, " - Public API\n" + public_api_summary diff --git a/docs/docs.py b/docs/docs.py index 96dc9f18d6..b2ebbdcd0f 100644 --- a/docs/docs.py +++ b/docs/docs.py @@ -20,7 +20,7 @@ CONFIG = BASE_DIR / "mkdocs.yml" DOCS_DIR = BASE_DIR / "docs" LANGUAGES_DIRS = tuple( - filter(lambda f: f.is_dir() and f.name not in IGNORE_DIRS, DOCS_DIR.iterdir()) + filter(lambda f: f.is_dir() and f.name not in IGNORE_DIRS, DOCS_DIR.iterdir()), ) BUILD_DIR = BASE_DIR / "site" @@ -34,13 +34,13 @@ config = load_config(str(CONFIG)) -DEV_SERVER = str(config.get("dev_addr", "0.0.0.0:8008")) +DEV_SERVER = str(config.get("dev_addr", "0.0.0.0:8000")) app = typer.Typer() @app.command() -def preview(): +def preview() -> None: """A quick server to preview a built site with translations. For development, prefer the command live (or just mkdocs serve). @@ -59,9 +59,9 @@ def preview(): @app.command() def live( - port: Annotated[Optional[str], typer.Argument()] = None, + port: Annotated[str | None, typer.Argument()] = None, fast: bool = False, -): +) -> None: """Start mkdocs preview with hotreload.""" if fast: _build_fast() @@ -76,31 +76,31 @@ def live( @app.command() -def build(): +def build() -> None: """Build documentation in full preview.""" _build() @app.command() -def build_fast(): +def build_fast() -> None: """Build documentation without API References.""" _build_fast() @app.command() -def build_api_docs(): +def build_api_docs() -> None: """Build api docs for faststream.""" typer.echo("Updating API docs") create_api_docs() @app.command() -def build_navigation(): +def build_navigation() -> None: typer.echo("Updating Navigation with empty API") render_navigation("", "") -def _build_fast(): +def _build_fast() -> None: typer.echo("Removing API directory") remove_api_dir() @@ -110,7 +110,7 @@ def _build_fast(): subprocess.run(["mkdocs", "build", "--site-dir", BUILD_DIR], check=True) -def _build(): +def _build() -> None: typer.echo("Updating Reference") build_api_docs() diff --git a/docs/docs/SUMMARY.md b/docs/docs/SUMMARY.md index a69746bfd5..8aca523fd7 100644 --- a/docs/docs/SUMMARY.md +++ b/docs/docs/SUMMARY.md @@ -42,6 +42,7 @@ search: - [FastAPI Plugin](getting-started/integrations/fastapi/index.md) - [Django](getting-started/integrations/django/index.md) - [CLI](getting-started/cli/index.md) + - [Acknowledgement](getting-started/acknowledgement.md) - [ASGI](getting-started/asgi.md) - Observability - [Healthcheks](getting-started/observability/healthcheks.md) @@ -125,6 +126,7 @@ search: - [Reference - Code API](api/index.md) - Public API - faststream + - [AckPolicy](public_api/faststream/AckPolicy.md) - [BaseMiddleware](public_api/faststream/BaseMiddleware.md) - [Context](public_api/faststream/Context.md) - [Depends](public_api/faststream/Depends.md) @@ -141,9 +143,6 @@ search: - [get](public_api/faststream/asgi/get.md) - [make_asyncapi_asgi](public_api/faststream/asgi/make_asyncapi_asgi.md) - [make_ping_asgi](public_api/faststream/asgi/make_ping_asgi.md) - - asyncapi - - [get_app_schema](public_api/faststream/asyncapi/get_app_schema.md) - - [get_asyncapi_html](public_api/faststream/asyncapi/get_asyncapi_html.md) - confluent - [KafkaBroker](public_api/faststream/confluent/KafkaBroker.md) - [KafkaPublisher](public_api/faststream/confluent/KafkaPublisher.md) @@ -177,6 +176,7 @@ search: - [NatsRouter](public_api/faststream/nats/NatsRouter.md) - [ObjWatch](public_api/faststream/nats/ObjWatch.md) - [Placement](public_api/faststream/nats/Placement.md) + - [PubAck](public_api/faststream/nats/PubAck.md) - [PullSub](public_api/faststream/nats/PullSub.md) - [RePublish](public_api/faststream/nats/RePublish.md) - [ReplayPolicy](public_api/faststream/nats/ReplayPolicy.md) @@ -201,7 +201,6 @@ search: - [RabbitResponse](public_api/faststream/rabbit/RabbitResponse.md) - [RabbitRoute](public_api/faststream/rabbit/RabbitRoute.md) - [RabbitRouter](public_api/faststream/rabbit/RabbitRouter.md) - - [ReplyConfig](public_api/faststream/rabbit/ReplyConfig.md) - [TestApp](public_api/faststream/rabbit/TestApp.md) - [TestRabbitBroker](public_api/faststream/rabbit/TestRabbitBroker.md) - redis @@ -217,6 +216,7 @@ search: - [TestRedisBroker](public_api/faststream/redis/TestRedisBroker.md) - All API - faststream + - [AckPolicy](api/faststream/AckPolicy.md) - [BaseMiddleware](api/faststream/BaseMiddleware.md) - [Context](api/faststream/Context.md) - [Depends](api/faststream/Depends.md) @@ -229,7 +229,6 @@ search: - [apply_types](api/faststream/apply_types.md) - app - [FastStream](api/faststream/app/FastStream.md) - - [catch_startup_validation_error](api/faststream/app/catch_startup_validation_error.md) - asgi - [AsgiFastStream](api/faststream/asgi/AsgiFastStream.md) - [AsgiResponse](api/faststream/asgi/AsgiResponse.md) @@ -238,6 +237,9 @@ search: - [make_ping_asgi](api/faststream/asgi/make_ping_asgi.md) - app - [AsgiFastStream](api/faststream/asgi/app/AsgiFastStream.md) + - [CliRunState](api/faststream/asgi/app/CliRunState.md) + - [OuterRunState](api/faststream/asgi/app/OuterRunState.md) + - [ServerState](api/faststream/asgi/app/ServerState.md) - [cast_uvicorn_params](api/faststream/asgi/app/cast_uvicorn_params.md) - factories - [make_asyncapi_asgi](api/faststream/asgi/factories/make_asyncapi_asgi.md) @@ -250,252 +252,6 @@ search: - [AsgiResponse](api/faststream/asgi/response/AsgiResponse.md) - websocket - [WebSocketClose](api/faststream/asgi/websocket/WebSocketClose.md) - - asyncapi - - [get_app_schema](api/faststream/asyncapi/get_app_schema.md) - - [get_asyncapi_html](api/faststream/asyncapi/get_asyncapi_html.md) - - abc - - [AsyncAPIOperation](api/faststream/asyncapi/abc/AsyncAPIOperation.md) - - generate - - [get_app_schema](api/faststream/asyncapi/generate/get_app_schema.md) - - [get_asgi_routes](api/faststream/asyncapi/generate/get_asgi_routes.md) - - [get_broker_channels](api/faststream/asyncapi/generate/get_broker_channels.md) - - [get_broker_server](api/faststream/asyncapi/generate/get_broker_server.md) - - message - - [get_model_schema](api/faststream/asyncapi/message/get_model_schema.md) - - [get_response_schema](api/faststream/asyncapi/message/get_response_schema.md) - - [parse_handler_params](api/faststream/asyncapi/message/parse_handler_params.md) - - proto - - [AsyncAPIApplication](api/faststream/asyncapi/proto/AsyncAPIApplication.md) - - [AsyncAPIProto](api/faststream/asyncapi/proto/AsyncAPIProto.md) - - schema - - [Channel](api/faststream/asyncapi/schema/Channel.md) - - [ChannelBinding](api/faststream/asyncapi/schema/ChannelBinding.md) - - [Components](api/faststream/asyncapi/schema/Components.md) - - [Contact](api/faststream/asyncapi/schema/Contact.md) - - [ContactDict](api/faststream/asyncapi/schema/ContactDict.md) - - [CorrelationId](api/faststream/asyncapi/schema/CorrelationId.md) - - [ExternalDocs](api/faststream/asyncapi/schema/ExternalDocs.md) - - [ExternalDocsDict](api/faststream/asyncapi/schema/ExternalDocsDict.md) - - [Info](api/faststream/asyncapi/schema/Info.md) - - [License](api/faststream/asyncapi/schema/License.md) - - [LicenseDict](api/faststream/asyncapi/schema/LicenseDict.md) - - [Message](api/faststream/asyncapi/schema/Message.md) - - [Operation](api/faststream/asyncapi/schema/Operation.md) - - [OperationBinding](api/faststream/asyncapi/schema/OperationBinding.md) - - [Reference](api/faststream/asyncapi/schema/Reference.md) - - [Schema](api/faststream/asyncapi/schema/Schema.md) - - [SecuritySchemaComponent](api/faststream/asyncapi/schema/SecuritySchemaComponent.md) - - [Server](api/faststream/asyncapi/schema/Server.md) - - [ServerBinding](api/faststream/asyncapi/schema/ServerBinding.md) - - [Tag](api/faststream/asyncapi/schema/Tag.md) - - [TagDict](api/faststream/asyncapi/schema/TagDict.md) - - bindings - - [ChannelBinding](api/faststream/asyncapi/schema/bindings/ChannelBinding.md) - - [OperationBinding](api/faststream/asyncapi/schema/bindings/OperationBinding.md) - - [ServerBinding](api/faststream/asyncapi/schema/bindings/ServerBinding.md) - - amqp - - [ChannelBinding](api/faststream/asyncapi/schema/bindings/amqp/ChannelBinding.md) - - [Exchange](api/faststream/asyncapi/schema/bindings/amqp/Exchange.md) - - [OperationBinding](api/faststream/asyncapi/schema/bindings/amqp/OperationBinding.md) - - [Queue](api/faststream/asyncapi/schema/bindings/amqp/Queue.md) - - [ServerBinding](api/faststream/asyncapi/schema/bindings/amqp/ServerBinding.md) - - http - - [OperationBinding](api/faststream/asyncapi/schema/bindings/http/OperationBinding.md) - - kafka - - [ChannelBinding](api/faststream/asyncapi/schema/bindings/kafka/ChannelBinding.md) - - [OperationBinding](api/faststream/asyncapi/schema/bindings/kafka/OperationBinding.md) - - [ServerBinding](api/faststream/asyncapi/schema/bindings/kafka/ServerBinding.md) - - main - - [ChannelBinding](api/faststream/asyncapi/schema/bindings/main/ChannelBinding.md) - - [OperationBinding](api/faststream/asyncapi/schema/bindings/main/OperationBinding.md) - - [ServerBinding](api/faststream/asyncapi/schema/bindings/main/ServerBinding.md) - - nats - - [ChannelBinding](api/faststream/asyncapi/schema/bindings/nats/ChannelBinding.md) - - [OperationBinding](api/faststream/asyncapi/schema/bindings/nats/OperationBinding.md) - - [ServerBinding](api/faststream/asyncapi/schema/bindings/nats/ServerBinding.md) - - redis - - [ChannelBinding](api/faststream/asyncapi/schema/bindings/redis/ChannelBinding.md) - - [OperationBinding](api/faststream/asyncapi/schema/bindings/redis/OperationBinding.md) - - [ServerBinding](api/faststream/asyncapi/schema/bindings/redis/ServerBinding.md) - - sqs - - [ChannelBinding](api/faststream/asyncapi/schema/bindings/sqs/ChannelBinding.md) - - [OperationBinding](api/faststream/asyncapi/schema/bindings/sqs/OperationBinding.md) - - [ServerBinding](api/faststream/asyncapi/schema/bindings/sqs/ServerBinding.md) - - channels - - [Channel](api/faststream/asyncapi/schema/channels/Channel.md) - - info - - [Contact](api/faststream/asyncapi/schema/info/Contact.md) - - [ContactDict](api/faststream/asyncapi/schema/info/ContactDict.md) - - [Info](api/faststream/asyncapi/schema/info/Info.md) - - [License](api/faststream/asyncapi/schema/info/License.md) - - [LicenseDict](api/faststream/asyncapi/schema/info/LicenseDict.md) - - main - - [Components](api/faststream/asyncapi/schema/main/Components.md) - - [Schema](api/faststream/asyncapi/schema/main/Schema.md) - - message - - [CorrelationId](api/faststream/asyncapi/schema/message/CorrelationId.md) - - [Message](api/faststream/asyncapi/schema/message/Message.md) - - operations - - [Operation](api/faststream/asyncapi/schema/operations/Operation.md) - - security - - [OauthFlowObj](api/faststream/asyncapi/schema/security/OauthFlowObj.md) - - [OauthFlows](api/faststream/asyncapi/schema/security/OauthFlows.md) - - [SecuritySchemaComponent](api/faststream/asyncapi/schema/security/SecuritySchemaComponent.md) - - servers - - [Server](api/faststream/asyncapi/schema/servers/Server.md) - - [ServerVariable](api/faststream/asyncapi/schema/servers/ServerVariable.md) - - utils - - [ExternalDocs](api/faststream/asyncapi/schema/utils/ExternalDocs.md) - - [ExternalDocsDict](api/faststream/asyncapi/schema/utils/ExternalDocsDict.md) - - [Parameter](api/faststream/asyncapi/schema/utils/Parameter.md) - - [Reference](api/faststream/asyncapi/schema/utils/Reference.md) - - [Tag](api/faststream/asyncapi/schema/utils/Tag.md) - - [TagDict](api/faststream/asyncapi/schema/utils/TagDict.md) - - site - - [get_asyncapi_html](api/faststream/asyncapi/site/get_asyncapi_html.md) - - [serve_app](api/faststream/asyncapi/site/serve_app.md) - - utils - - [resolve_payloads](api/faststream/asyncapi/utils/resolve_payloads.md) - - [to_camelcase](api/faststream/asyncapi/utils/to_camelcase.md) - - broker - - acknowledgement_watcher - - [BaseWatcher](api/faststream/broker/acknowledgement_watcher/BaseWatcher.md) - - [CounterWatcher](api/faststream/broker/acknowledgement_watcher/CounterWatcher.md) - - [EndlessWatcher](api/faststream/broker/acknowledgement_watcher/EndlessWatcher.md) - - [OneTryWatcher](api/faststream/broker/acknowledgement_watcher/OneTryWatcher.md) - - [WatcherContext](api/faststream/broker/acknowledgement_watcher/WatcherContext.md) - - [get_watcher](api/faststream/broker/acknowledgement_watcher/get_watcher.md) - - core - - abc - - [ABCBroker](api/faststream/broker/core/abc/ABCBroker.md) - - logging - - [LoggingBroker](api/faststream/broker/core/logging/LoggingBroker.md) - - usecase - - [BrokerUsecase](api/faststream/broker/core/usecase/BrokerUsecase.md) - - fastapi - - [StreamMessage](api/faststream/broker/fastapi/StreamMessage.md) - - [StreamRouter](api/faststream/broker/fastapi/StreamRouter.md) - - config - - [FastAPIConfig](api/faststream/broker/fastapi/config/FastAPIConfig.md) - - context - - [Context](api/faststream/broker/fastapi/context/Context.md) - - get_dependant - - [get_fastapi_dependant](api/faststream/broker/fastapi/get_dependant/get_fastapi_dependant.md) - - [get_fastapi_native_dependant](api/faststream/broker/fastapi/get_dependant/get_fastapi_native_dependant.md) - - [has_forbidden_types](api/faststream/broker/fastapi/get_dependant/has_forbidden_types.md) - - [is_faststream_decorated](api/faststream/broker/fastapi/get_dependant/is_faststream_decorated.md) - - [mark_faststream_decorated](api/faststream/broker/fastapi/get_dependant/mark_faststream_decorated.md) - - route - - [StreamMessage](api/faststream/broker/fastapi/route/StreamMessage.md) - - [build_faststream_to_fastapi_parser](api/faststream/broker/fastapi/route/build_faststream_to_fastapi_parser.md) - - [make_fastapi_execution](api/faststream/broker/fastapi/route/make_fastapi_execution.md) - - [wrap_callable_to_fastapi_compatible](api/faststream/broker/fastapi/route/wrap_callable_to_fastapi_compatible.md) - - router - - [StreamRouter](api/faststream/broker/fastapi/router/StreamRouter.md) - - message - - [AckStatus](api/faststream/broker/message/AckStatus.md) - - [SourceType](api/faststream/broker/message/SourceType.md) - - [StreamMessage](api/faststream/broker/message/StreamMessage.md) - - [decode_message](api/faststream/broker/message/decode_message.md) - - [encode_message](api/faststream/broker/message/encode_message.md) - - [gen_cor_id](api/faststream/broker/message/gen_cor_id.md) - - middlewares - - [BaseMiddleware](api/faststream/broker/middlewares/BaseMiddleware.md) - - [ExceptionMiddleware](api/faststream/broker/middlewares/ExceptionMiddleware.md) - - base - - [BaseMiddleware](api/faststream/broker/middlewares/base/BaseMiddleware.md) - - exception - - [BaseExceptionMiddleware](api/faststream/broker/middlewares/exception/BaseExceptionMiddleware.md) - - [ExceptionMiddleware](api/faststream/broker/middlewares/exception/ExceptionMiddleware.md) - - [ignore_handler](api/faststream/broker/middlewares/exception/ignore_handler.md) - - logging - - [CriticalLogMiddleware](api/faststream/broker/middlewares/logging/CriticalLogMiddleware.md) - - proto - - [EndpointProto](api/faststream/broker/proto/EndpointProto.md) - - [SetupAble](api/faststream/broker/proto/SetupAble.md) - - publisher - - fake - - [FakePublisher](api/faststream/broker/publisher/fake/FakePublisher.md) - - proto - - [BasePublisherProto](api/faststream/broker/publisher/proto/BasePublisherProto.md) - - [ProducerProto](api/faststream/broker/publisher/proto/ProducerProto.md) - - [PublisherProto](api/faststream/broker/publisher/proto/PublisherProto.md) - - usecase - - [PublisherUsecase](api/faststream/broker/publisher/usecase/PublisherUsecase.md) - - response - - [Response](api/faststream/broker/response/Response.md) - - [ensure_response](api/faststream/broker/response/ensure_response.md) - - router - - [ArgsContainer](api/faststream/broker/router/ArgsContainer.md) - - [BrokerRouter](api/faststream/broker/router/BrokerRouter.md) - - [SubscriberRoute](api/faststream/broker/router/SubscriberRoute.md) - - schemas - - [NameRequired](api/faststream/broker/schemas/NameRequired.md) - - subscriber - - call_item - - [HandlerItem](api/faststream/broker/subscriber/call_item/HandlerItem.md) - - mixins - - [ConcurrentMixin](api/faststream/broker/subscriber/mixins/ConcurrentMixin.md) - - [TasksMixin](api/faststream/broker/subscriber/mixins/TasksMixin.md) - - proto - - [SubscriberProto](api/faststream/broker/subscriber/proto/SubscriberProto.md) - - usecase - - [SubscriberUsecase](api/faststream/broker/subscriber/usecase/SubscriberUsecase.md) - - types - - [PublisherMiddleware](api/faststream/broker/types/PublisherMiddleware.md) - - utils - - [MultiLock](api/faststream/broker/utils/MultiLock.md) - - [default_filter](api/faststream/broker/utils/default_filter.md) - - [get_watcher_context](api/faststream/broker/utils/get_watcher_context.md) - - [process_msg](api/faststream/broker/utils/process_msg.md) - - [resolve_custom_func](api/faststream/broker/utils/resolve_custom_func.md) - - wrapper - - call - - [HandlerCallWrapper](api/faststream/broker/wrapper/call/HandlerCallWrapper.md) - - proto - - [WrapperProto](api/faststream/broker/wrapper/proto/WrapperProto.md) - - cli - - docs - - app - - [gen](api/faststream/cli/docs/app/gen.md) - - [serve](api/faststream/cli/docs/app/serve.md) - - main - - [main](api/faststream/cli/main/main.md) - - [publish](api/faststream/cli/main/publish.md) - - [publish_message](api/faststream/cli/main/publish_message.md) - - [run](api/faststream/cli/main/run.md) - - [version_callback](api/faststream/cli/main/version_callback.md) - - supervisors - - asgi_multiprocess - - [ASGIMultiprocess](api/faststream/cli/supervisors/asgi_multiprocess/ASGIMultiprocess.md) - - [UvicornExtraConfig](api/faststream/cli/supervisors/asgi_multiprocess/UvicornExtraConfig.md) - - basereload - - [BaseReload](api/faststream/cli/supervisors/basereload/BaseReload.md) - - multiprocess - - [Multiprocess](api/faststream/cli/supervisors/multiprocess/Multiprocess.md) - - utils - - [get_subprocess](api/faststream/cli/supervisors/utils/get_subprocess.md) - - [set_exit](api/faststream/cli/supervisors/utils/set_exit.md) - - [subprocess_started](api/faststream/cli/supervisors/utils/subprocess_started.md) - - watchfiles - - [ExtendedFilter](api/faststream/cli/supervisors/watchfiles/ExtendedFilter.md) - - [WatchReloader](api/faststream/cli/supervisors/watchfiles/WatchReloader.md) - - utils - - imports - - [get_app_path](api/faststream/cli/utils/imports/get_app_path.md) - - [import_from_string](api/faststream/cli/utils/imports/import_from_string.md) - - [import_object](api/faststream/cli/utils/imports/import_object.md) - - [try_import_app](api/faststream/cli/utils/imports/try_import_app.md) - - logs - - [LogFiles](api/faststream/cli/utils/logs/LogFiles.md) - - [LogLevels](api/faststream/cli/utils/logs/LogLevels.md) - - [get_log_level](api/faststream/cli/utils/logs/get_log_level.md) - - [set_log_config](api/faststream/cli/utils/logs/set_log_config.md) - - [set_log_level](api/faststream/cli/utils/logs/set_log_level.md) - - parser - - [is_bind_arg](api/faststream/cli/utils/parser/is_bind_arg.md) - - [parse_cli_args](api/faststream/cli/utils/parser/parse_cli_args.md) - - [remove_prefix](api/faststream/cli/utils/parser/remove_prefix.md) - confluent - [KafkaBroker](api/faststream/confluent/KafkaBroker.md) - [KafkaPublisher](api/faststream/confluent/KafkaPublisher.md) @@ -507,37 +263,56 @@ search: - [TopicPartition](api/faststream/confluent/TopicPartition.md) - broker - [KafkaBroker](api/faststream/confluent/broker/KafkaBroker.md) + - [KafkaPublisher](api/faststream/confluent/broker/KafkaPublisher.md) + - [KafkaRoute](api/faststream/confluent/broker/KafkaRoute.md) + - [KafkaRouter](api/faststream/confluent/broker/KafkaRouter.md) - broker - [KafkaBroker](api/faststream/confluent/broker/broker/KafkaBroker.md) - logging - - [KafkaLoggingBroker](api/faststream/confluent/broker/logging/KafkaLoggingBroker.md) + - [KafkaParamsStorage](api/faststream/confluent/broker/logging/KafkaParamsStorage.md) - registrator - [KafkaRegistrator](api/faststream/confluent/broker/registrator/KafkaRegistrator.md) - - client - - [AsyncConfluentConsumer](api/faststream/confluent/client/AsyncConfluentConsumer.md) - - [AsyncConfluentProducer](api/faststream/confluent/client/AsyncConfluentProducer.md) - - [BatchBuilder](api/faststream/confluent/client/BatchBuilder.md) - - [check_msg_error](api/faststream/confluent/client/check_msg_error.md) - - [create_topics](api/faststream/confluent/client/create_topics.md) - - config - - [BrokerAddressFamily](api/faststream/confluent/config/BrokerAddressFamily.md) - - [BuiltinFeatures](api/faststream/confluent/config/BuiltinFeatures.md) - - [ClientDNSLookup](api/faststream/confluent/config/ClientDNSLookup.md) - - [CompressionCodec](api/faststream/confluent/config/CompressionCodec.md) - - [CompressionType](api/faststream/confluent/config/CompressionType.md) - - [ConfluentConfig](api/faststream/confluent/config/ConfluentConfig.md) - - [ConfluentFastConfig](api/faststream/confluent/config/ConfluentFastConfig.md) - - [Debug](api/faststream/confluent/config/Debug.md) - - [GroupProtocol](api/faststream/confluent/config/GroupProtocol.md) - - [IsolationLevel](api/faststream/confluent/config/IsolationLevel.md) - - [OffsetStoreMethod](api/faststream/confluent/config/OffsetStoreMethod.md) - - [SASLOAUTHBearerMethod](api/faststream/confluent/config/SASLOAUTHBearerMethod.md) - - [SecurityProtocol](api/faststream/confluent/config/SecurityProtocol.md) + - router + - [KafkaPublisher](api/faststream/confluent/broker/router/KafkaPublisher.md) + - [KafkaRoute](api/faststream/confluent/broker/router/KafkaRoute.md) + - [KafkaRouter](api/faststream/confluent/broker/router/KafkaRouter.md) + - configs + - [KafkaBrokerConfig](api/faststream/confluent/configs/KafkaBrokerConfig.md) + - broker + - [ConsumerBuilder](api/faststream/confluent/configs/broker/ConsumerBuilder.md) + - [KafkaBrokerConfig](api/faststream/confluent/configs/broker/KafkaBrokerConfig.md) - fastapi - [Context](api/faststream/confluent/fastapi/Context.md) - [KafkaRouter](api/faststream/confluent/fastapi/KafkaRouter.md) - fastapi - [KafkaRouter](api/faststream/confluent/fastapi/fastapi/KafkaRouter.md) + - helpers + - [AdminService](api/faststream/confluent/helpers/AdminService.md) + - [AsyncConfluentConsumer](api/faststream/confluent/helpers/AsyncConfluentConsumer.md) + - [AsyncConfluentProducer](api/faststream/confluent/helpers/AsyncConfluentProducer.md) + - [ConfluentFastConfig](api/faststream/confluent/helpers/ConfluentFastConfig.md) + - admin + - [AdminService](api/faststream/confluent/helpers/admin/AdminService.md) + - [CreateResult](api/faststream/confluent/helpers/admin/CreateResult.md) + - client + - [AsyncConfluentConsumer](api/faststream/confluent/helpers/client/AsyncConfluentConsumer.md) + - [AsyncConfluentProducer](api/faststream/confluent/helpers/client/AsyncConfluentProducer.md) + - [BatchBuilder](api/faststream/confluent/helpers/client/BatchBuilder.md) + - [check_msg_error](api/faststream/confluent/helpers/client/check_msg_error.md) + - config + - [BrokerAddressFamily](api/faststream/confluent/helpers/config/BrokerAddressFamily.md) + - [BuiltinFeatures](api/faststream/confluent/helpers/config/BuiltinFeatures.md) + - [ClientDNSLookup](api/faststream/confluent/helpers/config/ClientDNSLookup.md) + - [CompressionCodec](api/faststream/confluent/helpers/config/CompressionCodec.md) + - [CompressionType](api/faststream/confluent/helpers/config/CompressionType.md) + - [ConfluentConfig](api/faststream/confluent/helpers/config/ConfluentConfig.md) + - [ConfluentFastConfig](api/faststream/confluent/helpers/config/ConfluentFastConfig.md) + - [Debug](api/faststream/confluent/helpers/config/Debug.md) + - [GroupProtocol](api/faststream/confluent/helpers/config/GroupProtocol.md) + - [IsolationLevel](api/faststream/confluent/helpers/config/IsolationLevel.md) + - [OffsetStoreMethod](api/faststream/confluent/helpers/config/OffsetStoreMethod.md) + - [SASLOAUTHBearerMethod](api/faststream/confluent/helpers/config/SASLOAUTHBearerMethod.md) + - [SecurityProtocol](api/faststream/confluent/helpers/config/SecurityProtocol.md) - message - [ConsumerProtocol](api/faststream/confluent/message/ConsumerProtocol.md) - [FakeConsumer](api/faststream/confluent/message/FakeConsumer.md) @@ -563,40 +338,44 @@ search: - [ConfluentMetricsSettingsProvider](api/faststream/confluent/prometheus/provider/ConfluentMetricsSettingsProvider.md) - [settings_provider_factory](api/faststream/confluent/prometheus/provider/settings_provider_factory.md) - publisher - - asyncapi - - [AsyncAPIBatchPublisher](api/faststream/confluent/publisher/asyncapi/AsyncAPIBatchPublisher.md) - - [AsyncAPIDefaultPublisher](api/faststream/confluent/publisher/asyncapi/AsyncAPIDefaultPublisher.md) - - [AsyncAPIPublisher](api/faststream/confluent/publisher/asyncapi/AsyncAPIPublisher.md) + - config + - [KafkaPublisherConfig](api/faststream/confluent/publisher/config/KafkaPublisherConfig.md) + - [KafkaPublisherSpecificationConfig](api/faststream/confluent/publisher/config/KafkaPublisherSpecificationConfig.md) + - factory + - [create_publisher](api/faststream/confluent/publisher/factory/create_publisher.md) + - fake + - [KafkaFakePublisher](api/faststream/confluent/publisher/fake/KafkaFakePublisher.md) - producer - [AsyncConfluentFastProducer](api/faststream/confluent/publisher/producer/AsyncConfluentFastProducer.md) + - [AsyncConfluentFastProducerImpl](api/faststream/confluent/publisher/producer/AsyncConfluentFastProducerImpl.md) + - [FakeConfluentFastProducer](api/faststream/confluent/publisher/producer/FakeConfluentFastProducer.md) + - specification + - [KafkaPublisherSpecification](api/faststream/confluent/publisher/specification/KafkaPublisherSpecification.md) + - state + - [EmptyProducerState](api/faststream/confluent/publisher/state/EmptyProducerState.md) + - [ProducerState](api/faststream/confluent/publisher/state/ProducerState.md) + - [RealProducer](api/faststream/confluent/publisher/state/RealProducer.md) - usecase - [BatchPublisher](api/faststream/confluent/publisher/usecase/BatchPublisher.md) - [DefaultPublisher](api/faststream/confluent/publisher/usecase/DefaultPublisher.md) - [LogicPublisher](api/faststream/confluent/publisher/usecase/LogicPublisher.md) - response + - [KafkaPublishCommand](api/faststream/confluent/response/KafkaPublishCommand.md) - [KafkaResponse](api/faststream/confluent/response/KafkaResponse.md) - - router - - [KafkaPublisher](api/faststream/confluent/router/KafkaPublisher.md) - - [KafkaRoute](api/faststream/confluent/router/KafkaRoute.md) - - [KafkaRouter](api/faststream/confluent/router/KafkaRouter.md) - schemas - [TopicPartition](api/faststream/confluent/schemas/TopicPartition.md) - - params - - [ConsumerConnectionParams](api/faststream/confluent/schemas/params/ConsumerConnectionParams.md) - - [SecurityOptions](api/faststream/confluent/schemas/params/SecurityOptions.md) - partition - [TopicPartition](api/faststream/confluent/schemas/partition/TopicPartition.md) - security - [parse_security](api/faststream/confluent/security/parse_security.md) - subscriber - - asyncapi - - [AsyncAPIBatchSubscriber](api/faststream/confluent/subscriber/asyncapi/AsyncAPIBatchSubscriber.md) - - [AsyncAPIConcurrentDefaultSubscriber](api/faststream/confluent/subscriber/asyncapi/AsyncAPIConcurrentDefaultSubscriber.md) - - [AsyncAPIDefaultSubscriber](api/faststream/confluent/subscriber/asyncapi/AsyncAPIDefaultSubscriber.md) - - [AsyncAPISubscriber](api/faststream/confluent/subscriber/asyncapi/AsyncAPISubscriber.md) + - config + - [KafkaSubscriberConfig](api/faststream/confluent/subscriber/config/KafkaSubscriberConfig.md) + - [KafkaSubscriberSpecificationConfig](api/faststream/confluent/subscriber/config/KafkaSubscriberSpecificationConfig.md) - factory - - [create_publisher](api/faststream/confluent/subscriber/factory/create_publisher.md) - [create_subscriber](api/faststream/confluent/subscriber/factory/create_subscriber.md) + - specification + - [KafkaSubscriberSpecification](api/faststream/confluent/subscriber/specification/KafkaSubscriberSpecification.md) - usecase - [BatchSubscriber](api/faststream/confluent/subscriber/usecase/BatchSubscriber.md) - [ConcurrentDefaultSubscriber](api/faststream/confluent/subscriber/usecase/ConcurrentDefaultSubscriber.md) @@ -607,22 +386,22 @@ search: - [MockConfluentMessage](api/faststream/confluent/testing/MockConfluentMessage.md) - [TestKafkaBroker](api/faststream/confluent/testing/TestKafkaBroker.md) - [build_message](api/faststream/confluent/testing/build_message.md) - - constants - - [ContentTypes](api/faststream/constants/ContentTypes.md) - exceptions - [AckMessage](api/faststream/exceptions/AckMessage.md) + - [ContextError](api/faststream/exceptions/ContextError.md) - [FastStreamException](api/faststream/exceptions/FastStreamException.md) + - [FeatureNotSupportedException](api/faststream/exceptions/FeatureNotSupportedException.md) - [HandlerException](api/faststream/exceptions/HandlerException.md) - [IgnoredException](api/faststream/exceptions/IgnoredException.md) + - [IncorrectState](api/faststream/exceptions/IncorrectState.md) - [NackMessage](api/faststream/exceptions/NackMessage.md) - - [OperationForbiddenError](api/faststream/exceptions/OperationForbiddenError.md) - [RejectMessage](api/faststream/exceptions/RejectMessage.md) - [SetupError](api/faststream/exceptions/SetupError.md) - [SkipMessage](api/faststream/exceptions/SkipMessage.md) + - [StartupValidationError](api/faststream/exceptions/StartupValidationError.md) - [StopApplication](api/faststream/exceptions/StopApplication.md) - [StopConsume](api/faststream/exceptions/StopConsume.md) - [SubscriberNotFound](api/faststream/exceptions/SubscriberNotFound.md) - - [ValidationError](api/faststream/exceptions/ValidationError.md) - kafka - [KafkaBroker](api/faststream/kafka/KafkaBroker.md) - [KafkaPublisher](api/faststream/kafka/KafkaPublisher.md) @@ -634,12 +413,23 @@ search: - [TopicPartition](api/faststream/kafka/TopicPartition.md) - broker - [KafkaBroker](api/faststream/kafka/broker/KafkaBroker.md) + - [KafkaPublisher](api/faststream/kafka/broker/KafkaPublisher.md) + - [KafkaRoute](api/faststream/kafka/broker/KafkaRoute.md) + - [KafkaRouter](api/faststream/kafka/broker/KafkaRouter.md) - broker - [KafkaBroker](api/faststream/kafka/broker/broker/KafkaBroker.md) - logging - - [KafkaLoggingBroker](api/faststream/kafka/broker/logging/KafkaLoggingBroker.md) + - [KafkaParamsStorage](api/faststream/kafka/broker/logging/KafkaParamsStorage.md) - registrator - [KafkaRegistrator](api/faststream/kafka/broker/registrator/KafkaRegistrator.md) + - router + - [KafkaPublisher](api/faststream/kafka/broker/router/KafkaPublisher.md) + - [KafkaRoute](api/faststream/kafka/broker/router/KafkaRoute.md) + - [KafkaRouter](api/faststream/kafka/broker/router/KafkaRouter.md) + - configs + - [KafkaBrokerConfig](api/faststream/kafka/configs/KafkaBrokerConfig.md) + - broker + - [KafkaBrokerConfig](api/faststream/kafka/configs/broker/KafkaBrokerConfig.md) - exceptions - [BatchBufferOverflowException](api/faststream/kafka/exceptions/BatchBufferOverflowException.md) - fastapi @@ -647,8 +437,12 @@ search: - [KafkaRouter](api/faststream/kafka/fastapi/KafkaRouter.md) - fastapi - [KafkaRouter](api/faststream/kafka/fastapi/fastapi/KafkaRouter.md) + - helpers + - [make_logging_listener](api/faststream/kafka/helpers/make_logging_listener.md) + - rebalance_listener + - [make_logging_listener](api/faststream/kafka/helpers/rebalance_listener/make_logging_listener.md) - listener - - [LoggingListenerProxy](api/faststream/kafka/listener/LoggingListenerProxy.md) + - [make_logging_listener](api/faststream/kafka/listener/make_logging_listener.md) - message - [ConsumerProtocol](api/faststream/kafka/message/ConsumerProtocol.md) - [FakeConsumer](api/faststream/kafka/message/FakeConsumer.md) @@ -677,22 +471,30 @@ search: - [KafkaMetricsSettingsProvider](api/faststream/kafka/prometheus/provider/KafkaMetricsSettingsProvider.md) - [settings_provider_factory](api/faststream/kafka/prometheus/provider/settings_provider_factory.md) - publisher - - asyncapi - - [AsyncAPIBatchPublisher](api/faststream/kafka/publisher/asyncapi/AsyncAPIBatchPublisher.md) - - [AsyncAPIDefaultPublisher](api/faststream/kafka/publisher/asyncapi/AsyncAPIDefaultPublisher.md) - - [AsyncAPIPublisher](api/faststream/kafka/publisher/asyncapi/AsyncAPIPublisher.md) + - config + - [KafkaPublisherConfig](api/faststream/kafka/publisher/config/KafkaPublisherConfig.md) + - [KafkaPublisherSpecificationConfig](api/faststream/kafka/publisher/config/KafkaPublisherSpecificationConfig.md) + - factory + - [create_publisher](api/faststream/kafka/publisher/factory/create_publisher.md) + - fake + - [KafkaFakePublisher](api/faststream/kafka/publisher/fake/KafkaFakePublisher.md) - producer - [AioKafkaFastProducer](api/faststream/kafka/publisher/producer/AioKafkaFastProducer.md) + - [AioKafkaFastProducerImpl](api/faststream/kafka/publisher/producer/AioKafkaFastProducerImpl.md) + - [FakeAioKafkaFastProducer](api/faststream/kafka/publisher/producer/FakeAioKafkaFastProducer.md) + - specification + - [KafkaPublisherSpecification](api/faststream/kafka/publisher/specification/KafkaPublisherSpecification.md) + - state + - [EmptyProducerState](api/faststream/kafka/publisher/state/EmptyProducerState.md) + - [ProducerState](api/faststream/kafka/publisher/state/ProducerState.md) + - [RealProducer](api/faststream/kafka/publisher/state/RealProducer.md) - usecase - [BatchPublisher](api/faststream/kafka/publisher/usecase/BatchPublisher.md) - [DefaultPublisher](api/faststream/kafka/publisher/usecase/DefaultPublisher.md) - [LogicPublisher](api/faststream/kafka/publisher/usecase/LogicPublisher.md) - response + - [KafkaPublishCommand](api/faststream/kafka/response/KafkaPublishCommand.md) - [KafkaResponse](api/faststream/kafka/response/KafkaResponse.md) - - router - - [KafkaPublisher](api/faststream/kafka/router/KafkaPublisher.md) - - [KafkaRoute](api/faststream/kafka/router/KafkaRoute.md) - - [KafkaRouter](api/faststream/kafka/router/KafkaRouter.md) - schemas - params - [AdminClientConnectionParams](api/faststream/kafka/schemas/params/AdminClientConnectionParams.md) @@ -700,15 +502,13 @@ search: - security - [parse_security](api/faststream/kafka/security/parse_security.md) - subscriber - - asyncapi - - [AsyncAPIBatchSubscriber](api/faststream/kafka/subscriber/asyncapi/AsyncAPIBatchSubscriber.md) - - [AsyncAPIConcurrentBetweenPartitionsSubscriber](api/faststream/kafka/subscriber/asyncapi/AsyncAPIConcurrentBetweenPartitionsSubscriber.md) - - [AsyncAPIConcurrentDefaultSubscriber](api/faststream/kafka/subscriber/asyncapi/AsyncAPIConcurrentDefaultSubscriber.md) - - [AsyncAPIDefaultSubscriber](api/faststream/kafka/subscriber/asyncapi/AsyncAPIDefaultSubscriber.md) - - [AsyncAPISubscriber](api/faststream/kafka/subscriber/asyncapi/AsyncAPISubscriber.md) + - config + - [KafkaSubscriberConfig](api/faststream/kafka/subscriber/config/KafkaSubscriberConfig.md) + - [KafkaSubscriberSpecificationConfig](api/faststream/kafka/subscriber/config/KafkaSubscriberSpecificationConfig.md) - factory - - [create_publisher](api/faststream/kafka/subscriber/factory/create_publisher.md) - [create_subscriber](api/faststream/kafka/subscriber/factory/create_subscriber.md) + - specification + - [KafkaSubscriberSpecification](api/faststream/kafka/subscriber/specification/KafkaSubscriberSpecification.md) - usecase - [BatchSubscriber](api/faststream/kafka/subscriber/usecase/BatchSubscriber.md) - [ConcurrentBetweenPartitionsSubscriber](api/faststream/kafka/subscriber/usecase/ConcurrentBetweenPartitionsSubscriber.md) @@ -716,17 +516,41 @@ search: - [DefaultSubscriber](api/faststream/kafka/subscriber/usecase/DefaultSubscriber.md) - [LogicSubscriber](api/faststream/kafka/subscriber/usecase/LogicSubscriber.md) - testing + - [FakeConsumer](api/faststream/kafka/testing/FakeConsumer.md) - [FakeProducer](api/faststream/kafka/testing/FakeProducer.md) - [TestKafkaBroker](api/faststream/kafka/testing/TestKafkaBroker.md) - [build_message](api/faststream/kafka/testing/build_message.md) - - log - - formatter - - [ColourizedFormatter](api/faststream/log/formatter/ColourizedFormatter.md) - - [expand_log_field](api/faststream/log/formatter/expand_log_field.md) + - message + - [AckStatus](api/faststream/message/AckStatus.md) + - [SourceType](api/faststream/message/SourceType.md) + - [StreamMessage](api/faststream/message/StreamMessage.md) + - [decode_message](api/faststream/message/decode_message.md) + - [encode_message](api/faststream/message/encode_message.md) + - [gen_cor_id](api/faststream/message/gen_cor_id.md) + - message + - [AckStatus](api/faststream/message/message/AckStatus.md) + - [StreamMessage](api/faststream/message/message/StreamMessage.md) + - source_type + - [SourceType](api/faststream/message/source_type/SourceType.md) + - utils + - [decode_message](api/faststream/message/utils/decode_message.md) + - [encode_message](api/faststream/message/utils/encode_message.md) + - [gen_cor_id](api/faststream/message/utils/gen_cor_id.md) + - middlewares + - [AckPolicy](api/faststream/middlewares/AckPolicy.md) + - [AcknowledgementMiddleware](api/faststream/middlewares/AcknowledgementMiddleware.md) + - [BaseMiddleware](api/faststream/middlewares/BaseMiddleware.md) + - [ExceptionMiddleware](api/faststream/middlewares/ExceptionMiddleware.md) + - acknowledgement + - conf + - [AckPolicy](api/faststream/middlewares/acknowledgement/conf/AckPolicy.md) + - middleware + - [AcknowledgementMiddleware](api/faststream/middlewares/acknowledgement/middleware/AcknowledgementMiddleware.md) + - exception + - [ExceptionMiddleware](api/faststream/middlewares/exception/ExceptionMiddleware.md) + - [ignore_handler](api/faststream/middlewares/exception/ignore_handler.md) - logging - - [ExtendedFilter](api/faststream/log/logging/ExtendedFilter.md) - - [get_broker_logger](api/faststream/log/logging/get_broker_logger.md) - - [set_logger_fmt](api/faststream/log/logging/set_logger_fmt.md) + - [CriticalLogMiddleware](api/faststream/middlewares/logging/CriticalLogMiddleware.md) - nats - [AckPolicy](api/faststream/nats/AckPolicy.md) - [ConsumerConfig](api/faststream/nats/ConsumerConfig.md) @@ -742,6 +566,7 @@ search: - [NatsRouter](api/faststream/nats/NatsRouter.md) - [ObjWatch](api/faststream/nats/ObjWatch.md) - [Placement](api/faststream/nats/Placement.md) + - [PubAck](api/faststream/nats/PubAck.md) - [PullSub](api/faststream/nats/PullSub.md) - [RePublish](api/faststream/nats/RePublish.md) - [ReplayPolicy](api/faststream/nats/ReplayPolicy.md) @@ -753,12 +578,25 @@ search: - [TestNatsBroker](api/faststream/nats/TestNatsBroker.md) - broker - [NatsBroker](api/faststream/nats/broker/NatsBroker.md) + - [NatsPublisher](api/faststream/nats/broker/NatsPublisher.md) + - [NatsRoute](api/faststream/nats/broker/NatsRoute.md) + - [NatsRouter](api/faststream/nats/broker/NatsRouter.md) - broker - [NatsBroker](api/faststream/nats/broker/broker/NatsBroker.md) - logging - - [NatsLoggingBroker](api/faststream/nats/broker/logging/NatsLoggingBroker.md) + - [NatsParamsStorage](api/faststream/nats/broker/logging/NatsParamsStorage.md) - registrator - [NatsRegistrator](api/faststream/nats/broker/registrator/NatsRegistrator.md) + - router + - [NatsPublisher](api/faststream/nats/broker/router/NatsPublisher.md) + - [NatsRoute](api/faststream/nats/broker/router/NatsRoute.md) + - [NatsRouter](api/faststream/nats/broker/router/NatsRouter.md) + - state + - [BrokerState](api/faststream/nats/broker/state/BrokerState.md) + - configs + - [NatsBrokerConfig](api/faststream/nats/configs/NatsBrokerConfig.md) + - broker + - [NatsBrokerConfig](api/faststream/nats/configs/broker/NatsBrokerConfig.md) - fastapi - [Context](api/faststream/nats/fastapi/Context.md) - [NatsRouter](api/faststream/nats/fastapi/NatsRouter.md) @@ -774,6 +612,10 @@ search: - [OSBucketDeclarer](api/faststream/nats/helpers/obj_storage_declarer/OSBucketDeclarer.md) - object_builder - [StreamBuilder](api/faststream/nats/helpers/object_builder/StreamBuilder.md) + - state + - [ConnectedState](api/faststream/nats/helpers/state/ConnectedState.md) + - [ConnectionState](api/faststream/nats/helpers/state/ConnectionState.md) + - [EmptyConnectionState](api/faststream/nats/helpers/state/EmptyConnectionState.md) - message - [NatsBatchMessage](api/faststream/nats/message/NatsBatchMessage.md) - [NatsKvMessage](api/faststream/nats/message/NatsKvMessage.md) @@ -805,23 +647,30 @@ search: - [NatsMetricsSettingsProvider](api/faststream/nats/prometheus/provider/NatsMetricsSettingsProvider.md) - [settings_provider_factory](api/faststream/nats/prometheus/provider/settings_provider_factory.md) - publisher - - asyncapi - - [AsyncAPIPublisher](api/faststream/nats/publisher/asyncapi/AsyncAPIPublisher.md) + - config + - [NatsPublisherConfig](api/faststream/nats/publisher/config/NatsPublisherConfig.md) + - [NatsPublisherSpecificationConfig](api/faststream/nats/publisher/config/NatsPublisherSpecificationConfig.md) + - factory + - [create_publisher](api/faststream/nats/publisher/factory/create_publisher.md) + - fake + - [NatsFakePublisher](api/faststream/nats/publisher/fake/NatsFakePublisher.md) - producer + - [FakeNatsFastProducer](api/faststream/nats/publisher/producer/FakeNatsFastProducer.md) - [NatsFastProducer](api/faststream/nats/publisher/producer/NatsFastProducer.md) + - [NatsFastProducerImpl](api/faststream/nats/publisher/producer/NatsFastProducerImpl.md) - [NatsJSFastProducer](api/faststream/nats/publisher/producer/NatsJSFastProducer.md) + - specification + - [NatsPublisherSpecification](api/faststream/nats/publisher/specification/NatsPublisherSpecification.md) - usecase - [LogicPublisher](api/faststream/nats/publisher/usecase/LogicPublisher.md) - response + - [NatsPublishCommand](api/faststream/nats/response/NatsPublishCommand.md) - [NatsResponse](api/faststream/nats/response/NatsResponse.md) - - router - - [NatsPublisher](api/faststream/nats/router/NatsPublisher.md) - - [NatsRoute](api/faststream/nats/router/NatsRoute.md) - - [NatsRouter](api/faststream/nats/router/NatsRouter.md) - schemas - [JStream](api/faststream/nats/schemas/JStream.md) - [KvWatch](api/faststream/nats/schemas/KvWatch.md) - [ObjWatch](api/faststream/nats/schemas/ObjWatch.md) + - [PubAck](api/faststream/nats/schemas/PubAck.md) - [PullSub](api/faststream/nats/schemas/PullSub.md) - js_stream - [JStream](api/faststream/nats/schemas/js_stream/JStream.md) @@ -836,39 +685,58 @@ search: - security - [parse_security](api/faststream/nats/security/parse_security.md) - subscriber - - asyncapi - - [AsyncAPIBatchPullStreamSubscriber](api/faststream/nats/subscriber/asyncapi/AsyncAPIBatchPullStreamSubscriber.md) - - [AsyncAPIConcurrentCoreSubscriber](api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentCoreSubscriber.md) - - [AsyncAPIConcurrentPullStreamSubscriber](api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentPullStreamSubscriber.md) - - [AsyncAPIConcurrentPushStreamSubscriber](api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentPushStreamSubscriber.md) - - [AsyncAPICoreSubscriber](api/faststream/nats/subscriber/asyncapi/AsyncAPICoreSubscriber.md) - - [AsyncAPIKeyValueWatchSubscriber](api/faststream/nats/subscriber/asyncapi/AsyncAPIKeyValueWatchSubscriber.md) - - [AsyncAPIObjStoreWatchSubscriber](api/faststream/nats/subscriber/asyncapi/AsyncAPIObjStoreWatchSubscriber.md) - - [AsyncAPIPullStreamSubscriber](api/faststream/nats/subscriber/asyncapi/AsyncAPIPullStreamSubscriber.md) - - [AsyncAPIStreamSubscriber](api/faststream/nats/subscriber/asyncapi/AsyncAPIStreamSubscriber.md) - - [AsyncAPISubscriber](api/faststream/nats/subscriber/asyncapi/AsyncAPISubscriber.md) + - adapters + - [UnsubscribeAdapter](api/faststream/nats/subscriber/adapters/UnsubscribeAdapter.md) + - [Unsubscriptable](api/faststream/nats/subscriber/adapters/Unsubscriptable.md) + - [Watchable](api/faststream/nats/subscriber/adapters/Watchable.md) + - config + - [NatsSubscriberConfig](api/faststream/nats/subscriber/config/NatsSubscriberConfig.md) + - [NatsSubscriberSpecificationConfig](api/faststream/nats/subscriber/config/NatsSubscriberSpecificationConfig.md) - factory - [create_subscriber](api/faststream/nats/subscriber/factory/create_subscriber.md) - - subscription - - [UnsubscribeAdapter](api/faststream/nats/subscriber/subscription/UnsubscribeAdapter.md) - - [Unsubscriptable](api/faststream/nats/subscriber/subscription/Unsubscriptable.md) - - [Watchable](api/faststream/nats/subscriber/subscription/Watchable.md) - - usecase - - [BatchPullStreamSubscriber](api/faststream/nats/subscriber/usecase/BatchPullStreamSubscriber.md) - - [ConcurrentCoreSubscriber](api/faststream/nats/subscriber/usecase/ConcurrentCoreSubscriber.md) - - [ConcurrentPullStreamSubscriber](api/faststream/nats/subscriber/usecase/ConcurrentPullStreamSubscriber.md) - - [ConcurrentPushStreamSubscriber](api/faststream/nats/subscriber/usecase/ConcurrentPushStreamSubscriber.md) - - [CoreSubscriber](api/faststream/nats/subscriber/usecase/CoreSubscriber.md) - - [KeyValueWatchSubscriber](api/faststream/nats/subscriber/usecase/KeyValueWatchSubscriber.md) - - [LogicSubscriber](api/faststream/nats/subscriber/usecase/LogicSubscriber.md) - - [ObjStoreWatchSubscriber](api/faststream/nats/subscriber/usecase/ObjStoreWatchSubscriber.md) - - [PullStreamSubscriber](api/faststream/nats/subscriber/usecase/PullStreamSubscriber.md) - - [PushStreamSubscription](api/faststream/nats/subscriber/usecase/PushStreamSubscription.md) + - specification + - [NatsSubscriberSpecification](api/faststream/nats/subscriber/specification/NatsSubscriberSpecification.md) + - [NotIncludeSpecifation](api/faststream/nats/subscriber/specification/NotIncludeSpecifation.md) + - state + - [ConnectedSubscriberState](api/faststream/nats/subscriber/state/ConnectedSubscriberState.md) + - [EmptySubscriberState](api/faststream/nats/subscriber/state/EmptySubscriberState.md) + - [SubscriberState](api/faststream/nats/subscriber/state/SubscriberState.md) + - usecases + - [BatchPullStreamSubscriber](api/faststream/nats/subscriber/usecases/BatchPullStreamSubscriber.md) + - [ConcurrentCoreSubscriber](api/faststream/nats/subscriber/usecases/ConcurrentCoreSubscriber.md) + - [ConcurrentPullStreamSubscriber](api/faststream/nats/subscriber/usecases/ConcurrentPullStreamSubscriber.md) + - [ConcurrentPushStreamSubscriber](api/faststream/nats/subscriber/usecases/ConcurrentPushStreamSubscriber.md) + - [CoreSubscriber](api/faststream/nats/subscriber/usecases/CoreSubscriber.md) + - [KeyValueWatchSubscriber](api/faststream/nats/subscriber/usecases/KeyValueWatchSubscriber.md) + - [LogicSubscriber](api/faststream/nats/subscriber/usecases/LogicSubscriber.md) + - [ObjStoreWatchSubscriber](api/faststream/nats/subscriber/usecases/ObjStoreWatchSubscriber.md) + - [PullStreamSubscriber](api/faststream/nats/subscriber/usecases/PullStreamSubscriber.md) + - [PushStreamSubscriber](api/faststream/nats/subscriber/usecases/PushStreamSubscriber.md) + - basic + - [DefaultSubscriber](api/faststream/nats/subscriber/usecases/basic/DefaultSubscriber.md) + - [LogicSubscriber](api/faststream/nats/subscriber/usecases/basic/LogicSubscriber.md) + - core_subscriber + - [ConcurrentCoreSubscriber](api/faststream/nats/subscriber/usecases/core_subscriber/ConcurrentCoreSubscriber.md) + - [CoreSubscriber](api/faststream/nats/subscriber/usecases/core_subscriber/CoreSubscriber.md) + - key_value_subscriber + - [KeyValueWatchSubscriber](api/faststream/nats/subscriber/usecases/key_value_subscriber/KeyValueWatchSubscriber.md) + - object_storage_subscriber + - [ObjStoreWatchSubscriber](api/faststream/nats/subscriber/usecases/object_storage_subscriber/ObjStoreWatchSubscriber.md) + - stream_basic + - [StreamSubscriber](api/faststream/nats/subscriber/usecases/stream_basic/StreamSubscriber.md) + - stream_pull_subscriber + - [BatchPullStreamSubscriber](api/faststream/nats/subscriber/usecases/stream_pull_subscriber/BatchPullStreamSubscriber.md) + - [ConcurrentPullStreamSubscriber](api/faststream/nats/subscriber/usecases/stream_pull_subscriber/ConcurrentPullStreamSubscriber.md) + - [PullStreamSubscriber](api/faststream/nats/subscriber/usecases/stream_pull_subscriber/PullStreamSubscriber.md) + - stream_push_subscriber + - [ConcurrentPushStreamSubscriber](api/faststream/nats/subscriber/usecases/stream_push_subscriber/ConcurrentPushStreamSubscriber.md) + - [PushStreamSubscriber](api/faststream/nats/subscriber/usecases/stream_push_subscriber/PushStreamSubscriber.md) - testing - [FakeProducer](api/faststream/nats/testing/FakeProducer.md) - [PatchedMessage](api/faststream/nats/testing/PatchedMessage.md) - [TestNatsBroker](api/faststream/nats/testing/TestNatsBroker.md) - [build_message](api/faststream/nats/testing/build_message.md) + - [change_producer](api/faststream/nats/testing/change_producer.md) - opentelemetry - [Baggage](api/faststream/opentelemetry/Baggage.md) - [TelemetryMiddleware](api/faststream/opentelemetry/TelemetryMiddleware.md) @@ -882,10 +750,21 @@ search: - [TelemetryMiddleware](api/faststream/opentelemetry/middleware/TelemetryMiddleware.md) - provider - [TelemetrySettingsProvider](api/faststream/opentelemetry/provider/TelemetrySettingsProvider.md) + - params + - [Context](api/faststream/params/Context.md) + - [Depends](api/faststream/params/Depends.md) + - [Header](api/faststream/params/Header.md) + - [Path](api/faststream/params/Path.md) + - no_cast + - [NoCastField](api/faststream/params/no_cast/NoCastField.md) + - params + - [Context](api/faststream/params/params/Context.md) + - [Header](api/faststream/params/params/Header.md) + - [Path](api/faststream/params/params/Path.md) - prometheus - - [BasePrometheusMiddleware](api/faststream/prometheus/BasePrometheusMiddleware.md) - [ConsumeAttrs](api/faststream/prometheus/ConsumeAttrs.md) - [MetricsSettingsProvider](api/faststream/prometheus/MetricsSettingsProvider.md) + - [PrometheusMiddleware](api/faststream/prometheus/PrometheusMiddleware.md) - container - [MetricsContainer](api/faststream/prometheus/container/MetricsContainer.md) - manager @@ -910,29 +789,50 @@ search: - [RabbitResponse](api/faststream/rabbit/RabbitResponse.md) - [RabbitRoute](api/faststream/rabbit/RabbitRoute.md) - [RabbitRouter](api/faststream/rabbit/RabbitRouter.md) - - [ReplyConfig](api/faststream/rabbit/ReplyConfig.md) - [TestApp](api/faststream/rabbit/TestApp.md) - [TestRabbitBroker](api/faststream/rabbit/TestRabbitBroker.md) - broker - [RabbitBroker](api/faststream/rabbit/broker/RabbitBroker.md) + - [RabbitPublisher](api/faststream/rabbit/broker/RabbitPublisher.md) + - [RabbitRoute](api/faststream/rabbit/broker/RabbitRoute.md) + - [RabbitRouter](api/faststream/rabbit/broker/RabbitRouter.md) - broker - [RabbitBroker](api/faststream/rabbit/broker/broker/RabbitBroker.md) - logging - - [RabbitLoggingBroker](api/faststream/rabbit/broker/logging/RabbitLoggingBroker.md) + - [RabbitParamsStorage](api/faststream/rabbit/broker/logging/RabbitParamsStorage.md) - registrator - [RabbitRegistrator](api/faststream/rabbit/broker/registrator/RabbitRegistrator.md) + - router + - [RabbitPublisher](api/faststream/rabbit/broker/router/RabbitPublisher.md) + - [RabbitRoute](api/faststream/rabbit/broker/router/RabbitRoute.md) + - [RabbitRouter](api/faststream/rabbit/broker/router/RabbitRouter.md) + - configs + - [RabbitBrokerConfig](api/faststream/rabbit/configs/RabbitBrokerConfig.md) + - base + - [RabbitConfig](api/faststream/rabbit/configs/base/RabbitConfig.md) + - [RabbitEndpointConfig](api/faststream/rabbit/configs/base/RabbitEndpointConfig.md) + - broker + - [RabbitBrokerConfig](api/faststream/rabbit/configs/broker/RabbitBrokerConfig.md) - fastapi - [Context](api/faststream/rabbit/fastapi/Context.md) - [RabbitRouter](api/faststream/rabbit/fastapi/RabbitRouter.md) - - router - - [RabbitRouter](api/faststream/rabbit/fastapi/router/RabbitRouter.md) + - fastapi + - [RabbitRouter](api/faststream/rabbit/fastapi/fastapi/RabbitRouter.md) - helpers - [ChannelManager](api/faststream/rabbit/helpers/ChannelManager.md) - [RabbitDeclarer](api/faststream/rabbit/helpers/RabbitDeclarer.md) - channel_manager - [ChannelManager](api/faststream/rabbit/helpers/channel_manager/ChannelManager.md) + - [ChannelManagerImpl](api/faststream/rabbit/helpers/channel_manager/ChannelManagerImpl.md) + - [FakeChannelManager](api/faststream/rabbit/helpers/channel_manager/FakeChannelManager.md) - declarer + - [FakeRabbitDeclarer](api/faststream/rabbit/helpers/declarer/FakeRabbitDeclarer.md) - [RabbitDeclarer](api/faststream/rabbit/helpers/declarer/RabbitDeclarer.md) + - [RabbitDeclarerImpl](api/faststream/rabbit/helpers/declarer/RabbitDeclarerImpl.md) + - state + - [ConnectedState](api/faststream/rabbit/helpers/state/ConnectedState.md) + - [ConnectionState](api/faststream/rabbit/helpers/state/ConnectionState.md) + - [EmptyConnectionState](api/faststream/rabbit/helpers/state/EmptyConnectionState.md) - message - [RabbitMessage](api/faststream/rabbit/message/RabbitMessage.md) - opentelemetry @@ -950,20 +850,33 @@ search: - provider - [RabbitMetricsSettingsProvider](api/faststream/rabbit/prometheus/provider/RabbitMetricsSettingsProvider.md) - publisher - - asyncapi - - [AsyncAPIPublisher](api/faststream/rabbit/publisher/asyncapi/AsyncAPIPublisher.md) + - [RabbitPublisher](api/faststream/rabbit/publisher/RabbitPublisher.md) + - config + - [RabbitPublisherConfig](api/faststream/rabbit/publisher/config/RabbitPublisherConfig.md) + - [RabbitPublisherSpecificationConfig](api/faststream/rabbit/publisher/config/RabbitPublisherSpecificationConfig.md) + - factory + - [create_publisher](api/faststream/rabbit/publisher/factory/create_publisher.md) + - fake + - [RabbitFakePublisher](api/faststream/rabbit/publisher/fake/RabbitFakePublisher.md) + - options + - [MessageOptions](api/faststream/rabbit/publisher/options/MessageOptions.md) + - [PublishKwargs](api/faststream/rabbit/publisher/options/PublishKwargs.md) + - [PublishOptions](api/faststream/rabbit/publisher/options/PublishOptions.md) + - [RequestPublishKwargs](api/faststream/rabbit/publisher/options/RequestPublishKwargs.md) - producer - [AioPikaFastProducer](api/faststream/rabbit/publisher/producer/AioPikaFastProducer.md) + - [AioPikaFastProducerImpl](api/faststream/rabbit/publisher/producer/AioPikaFastProducerImpl.md) + - [FakeAioPikaFastProducer](api/faststream/rabbit/publisher/producer/FakeAioPikaFastProducer.md) + - [LockState](api/faststream/rabbit/publisher/producer/LockState.md) + - [LockUnset](api/faststream/rabbit/publisher/producer/LockUnset.md) + - [RealLock](api/faststream/rabbit/publisher/producer/RealLock.md) + - specification + - [RabbitPublisherSpecification](api/faststream/rabbit/publisher/specification/RabbitPublisherSpecification.md) - usecase - - [LogicPublisher](api/faststream/rabbit/publisher/usecase/LogicPublisher.md) - - [PublishKwargs](api/faststream/rabbit/publisher/usecase/PublishKwargs.md) - - [RequestPublishKwargs](api/faststream/rabbit/publisher/usecase/RequestPublishKwargs.md) + - [RabbitPublisher](api/faststream/rabbit/publisher/usecase/RabbitPublisher.md) - response + - [RabbitPublishCommand](api/faststream/rabbit/response/RabbitPublishCommand.md) - [RabbitResponse](api/faststream/rabbit/response/RabbitResponse.md) - - router - - [RabbitPublisher](api/faststream/rabbit/router/RabbitPublisher.md) - - [RabbitRoute](api/faststream/rabbit/router/RabbitRoute.md) - - [RabbitRouter](api/faststream/rabbit/router/RabbitRouter.md) - schemas - [BaseRMQInformation](api/faststream/rabbit/schemas/BaseRMQInformation.md) - [Channel](api/faststream/rabbit/schemas/Channel.md) @@ -971,7 +884,6 @@ search: - [QueueType](api/faststream/rabbit/schemas/QueueType.md) - [RabbitExchange](api/faststream/rabbit/schemas/RabbitExchange.md) - [RabbitQueue](api/faststream/rabbit/schemas/RabbitQueue.md) - - [ReplyConfig](api/faststream/rabbit/schemas/ReplyConfig.md) - channel - [Channel](api/faststream/rabbit/schemas/channel/Channel.md) - constants @@ -991,17 +903,19 @@ search: - [SharedClassicAndQuorumQueueArgs](api/faststream/rabbit/schemas/queue/SharedClassicAndQuorumQueueArgs.md) - [StreamQueueArgs](api/faststream/rabbit/schemas/queue/StreamQueueArgs.md) - [StreamQueueSpecificArgs](api/faststream/rabbit/schemas/queue/StreamQueueSpecificArgs.md) - - reply - - [ReplyConfig](api/faststream/rabbit/schemas/reply/ReplyConfig.md) - security - [parse_security](api/faststream/rabbit/security/parse_security.md) - subscriber - - asyncapi - - [AsyncAPISubscriber](api/faststream/rabbit/subscriber/asyncapi/AsyncAPISubscriber.md) + - [RabbitSubscriber](api/faststream/rabbit/subscriber/RabbitSubscriber.md) + - config + - [RabbitSubscriberConfig](api/faststream/rabbit/subscriber/config/RabbitSubscriberConfig.md) + - [RabbitSubscriberSpecificationConfig](api/faststream/rabbit/subscriber/config/RabbitSubscriberSpecificationConfig.md) - factory - [create_subscriber](api/faststream/rabbit/subscriber/factory/create_subscriber.md) + - specification + - [RabbitSubscriberSpecification](api/faststream/rabbit/subscriber/specification/RabbitSubscriberSpecification.md) - usecase - - [LogicSubscriber](api/faststream/rabbit/subscriber/usecase/LogicSubscriber.md) + - [RabbitSubscriber](api/faststream/rabbit/subscriber/usecase/RabbitSubscriber.md) - testing - [FakeProducer](api/faststream/rabbit/testing/FakeProducer.md) - [PatchedMessage](api/faststream/rabbit/testing/PatchedMessage.md) @@ -1027,12 +941,28 @@ search: - annotations - [get_pipe](api/faststream/redis/annotations/get_pipe.md) - broker + - [RedisBroker](api/faststream/redis/broker/RedisBroker.md) + - [RedisPublisher](api/faststream/redis/broker/RedisPublisher.md) + - [RedisRoute](api/faststream/redis/broker/RedisRoute.md) + - [RedisRouter](api/faststream/redis/broker/RedisRouter.md) - broker - [RedisBroker](api/faststream/redis/broker/broker/RedisBroker.md) - logging - - [RedisLoggingBroker](api/faststream/redis/broker/logging/RedisLoggingBroker.md) + - [RedisParamsStorage](api/faststream/redis/broker/logging/RedisParamsStorage.md) - registrator - [RedisRegistrator](api/faststream/redis/broker/registrator/RedisRegistrator.md) + - router + - [RedisPublisher](api/faststream/redis/broker/router/RedisPublisher.md) + - [RedisRoute](api/faststream/redis/broker/router/RedisRoute.md) + - [RedisRouter](api/faststream/redis/broker/router/RedisRouter.md) + - configs + - [ConnectionState](api/faststream/redis/configs/ConnectionState.md) + - [RedisBrokerConfig](api/faststream/redis/configs/RedisBrokerConfig.md) + - broker + - [RedisBrokerConfig](api/faststream/redis/configs/broker/RedisBrokerConfig.md) + - [RedisRouterConfig](api/faststream/redis/configs/broker/RedisRouterConfig.md) + - state + - [ConnectionState](api/faststream/redis/configs/state/ConnectionState.md) - fastapi - [Context](api/faststream/redis/fastapi/Context.md) - [RedisRouter](api/faststream/redis/fastapi/RedisRouter.md) @@ -1043,14 +973,12 @@ search: - [BatchStreamMessage](api/faststream/redis/message/BatchStreamMessage.md) - [DefaultListMessage](api/faststream/redis/message/DefaultListMessage.md) - [DefaultStreamMessage](api/faststream/redis/message/DefaultStreamMessage.md) - - [ListMessage](api/faststream/redis/message/ListMessage.md) - [PubSubMessage](api/faststream/redis/message/PubSubMessage.md) - [RedisBatchListMessage](api/faststream/redis/message/RedisBatchListMessage.md) - [RedisBatchStreamMessage](api/faststream/redis/message/RedisBatchStreamMessage.md) - [RedisListMessage](api/faststream/redis/message/RedisListMessage.md) - [RedisMessage](api/faststream/redis/message/RedisMessage.md) - [RedisStreamMessage](api/faststream/redis/message/RedisStreamMessage.md) - - [StreamMessage](api/faststream/redis/message/StreamMessage.md) - [UnifyRedisDict](api/faststream/redis/message/UnifyRedisDict.md) - [UnifyRedisMessage](api/faststream/redis/message/UnifyRedisMessage.md) - opentelemetry @@ -1077,14 +1005,20 @@ search: - [RedisMetricsSettingsProvider](api/faststream/redis/prometheus/provider/RedisMetricsSettingsProvider.md) - [settings_provider_factory](api/faststream/redis/prometheus/provider/settings_provider_factory.md) - publisher - - asyncapi - - [AsyncAPIChannelPublisher](api/faststream/redis/publisher/asyncapi/AsyncAPIChannelPublisher.md) - - [AsyncAPIListBatchPublisher](api/faststream/redis/publisher/asyncapi/AsyncAPIListBatchPublisher.md) - - [AsyncAPIListPublisher](api/faststream/redis/publisher/asyncapi/AsyncAPIListPublisher.md) - - [AsyncAPIPublisher](api/faststream/redis/publisher/asyncapi/AsyncAPIPublisher.md) - - [AsyncAPIStreamPublisher](api/faststream/redis/publisher/asyncapi/AsyncAPIStreamPublisher.md) + - config + - [RedisPublisherConfig](api/faststream/redis/publisher/config/RedisPublisherConfig.md) + - [RedisPublisherSpecificationConfig](api/faststream/redis/publisher/config/RedisPublisherSpecificationConfig.md) + - factory + - [create_publisher](api/faststream/redis/publisher/factory/create_publisher.md) + - fake + - [RedisFakePublisher](api/faststream/redis/publisher/fake/RedisFakePublisher.md) - producer - [RedisFastProducer](api/faststream/redis/publisher/producer/RedisFastProducer.md) + - specification + - [ChannelPublisherSpecification](api/faststream/redis/publisher/specification/ChannelPublisherSpecification.md) + - [ListPublisherSpecification](api/faststream/redis/publisher/specification/ListPublisherSpecification.md) + - [RedisPublisherSpecification](api/faststream/redis/publisher/specification/RedisPublisherSpecification.md) + - [StreamPublisherSpecification](api/faststream/redis/publisher/specification/StreamPublisherSpecification.md) - usecase - [ChannelPublisher](api/faststream/redis/publisher/usecase/ChannelPublisher.md) - [ListBatchPublisher](api/faststream/redis/publisher/usecase/ListBatchPublisher.md) @@ -1092,11 +1026,9 @@ search: - [LogicPublisher](api/faststream/redis/publisher/usecase/LogicPublisher.md) - [StreamPublisher](api/faststream/redis/publisher/usecase/StreamPublisher.md) - response + - [DestinationType](api/faststream/redis/response/DestinationType.md) + - [RedisPublishCommand](api/faststream/redis/response/RedisPublishCommand.md) - [RedisResponse](api/faststream/redis/response/RedisResponse.md) - - router - - [RedisPublisher](api/faststream/redis/router/RedisPublisher.md) - - [RedisRoute](api/faststream/redis/router/RedisRoute.md) - - [RedisRouter](api/faststream/redis/router/RedisRouter.md) - schemas - [ListSub](api/faststream/redis/schemas/ListSub.md) - [PubSub](api/faststream/redis/schemas/PubSub.md) @@ -1104,7 +1036,6 @@ search: - list_sub - [ListSub](api/faststream/redis/schemas/list_sub/ListSub.md) - proto - - [RedisAsyncAPIProtocol](api/faststream/redis/schemas/proto/RedisAsyncAPIProtocol.md) - [validate_options](api/faststream/redis/schemas/proto/validate_options.md) - pub_sub - [PubSub](api/faststream/redis/schemas/pub_sub/PubSub.md) @@ -1113,22 +1044,40 @@ search: - security - [parse_security](api/faststream/redis/security/parse_security.md) - subscriber - - asyncapi - - [AsyncAPIChannelSubscriber](api/faststream/redis/subscriber/asyncapi/AsyncAPIChannelSubscriber.md) - - [AsyncAPIListBatchSubscriber](api/faststream/redis/subscriber/asyncapi/AsyncAPIListBatchSubscriber.md) - - [AsyncAPIListSubscriber](api/faststream/redis/subscriber/asyncapi/AsyncAPIListSubscriber.md) - - [AsyncAPIStreamBatchSubscriber](api/faststream/redis/subscriber/asyncapi/AsyncAPIStreamBatchSubscriber.md) - - [AsyncAPIStreamSubscriber](api/faststream/redis/subscriber/asyncapi/AsyncAPIStreamSubscriber.md) - - [AsyncAPISubscriber](api/faststream/redis/subscriber/asyncapi/AsyncAPISubscriber.md) + - config + - [RedisSubscriberConfig](api/faststream/redis/subscriber/config/RedisSubscriberConfig.md) + - [RedisSubscriberSpecificationConfig](api/faststream/redis/subscriber/config/RedisSubscriberSpecificationConfig.md) - factory - [create_subscriber](api/faststream/redis/subscriber/factory/create_subscriber.md) - - usecase - - [BatchListSubscriber](api/faststream/redis/subscriber/usecase/BatchListSubscriber.md) - - [BatchStreamSubscriber](api/faststream/redis/subscriber/usecase/BatchStreamSubscriber.md) - - [ChannelSubscriber](api/faststream/redis/subscriber/usecase/ChannelSubscriber.md) - - [ListSubscriber](api/faststream/redis/subscriber/usecase/ListSubscriber.md) - - [LogicSubscriber](api/faststream/redis/subscriber/usecase/LogicSubscriber.md) - - [StreamSubscriber](api/faststream/redis/subscriber/usecase/StreamSubscriber.md) + - specification + - [ChannelSubscriberSpecification](api/faststream/redis/subscriber/specification/ChannelSubscriberSpecification.md) + - [ListSubscriberSpecification](api/faststream/redis/subscriber/specification/ListSubscriberSpecification.md) + - [RedisSubscriberSpecification](api/faststream/redis/subscriber/specification/RedisSubscriberSpecification.md) + - [StreamSubscriberSpecification](api/faststream/redis/subscriber/specification/StreamSubscriberSpecification.md) + - usecases + - [ChannelConcurrentSubscriber](api/faststream/redis/subscriber/usecases/ChannelConcurrentSubscriber.md) + - [ChannelSubscriber](api/faststream/redis/subscriber/usecases/ChannelSubscriber.md) + - [ListBatchSubscriber](api/faststream/redis/subscriber/usecases/ListBatchSubscriber.md) + - [ListConcurrentSubscriber](api/faststream/redis/subscriber/usecases/ListConcurrentSubscriber.md) + - [ListSubscriber](api/faststream/redis/subscriber/usecases/ListSubscriber.md) + - [LogicSubscriber](api/faststream/redis/subscriber/usecases/LogicSubscriber.md) + - [StreamBatchSubscriber](api/faststream/redis/subscriber/usecases/StreamBatchSubscriber.md) + - [StreamConcurrentSubscriber](api/faststream/redis/subscriber/usecases/StreamConcurrentSubscriber.md) + - [StreamSubscriber](api/faststream/redis/subscriber/usecases/StreamSubscriber.md) + - basic + - [ConcurrentSubscriber](api/faststream/redis/subscriber/usecases/basic/ConcurrentSubscriber.md) + - [LogicSubscriber](api/faststream/redis/subscriber/usecases/basic/LogicSubscriber.md) + - channel_subscriber + - [ChannelConcurrentSubscriber](api/faststream/redis/subscriber/usecases/channel_subscriber/ChannelConcurrentSubscriber.md) + - [ChannelSubscriber](api/faststream/redis/subscriber/usecases/channel_subscriber/ChannelSubscriber.md) + - list_subscriber + - [ListBatchSubscriber](api/faststream/redis/subscriber/usecases/list_subscriber/ListBatchSubscriber.md) + - [ListConcurrentSubscriber](api/faststream/redis/subscriber/usecases/list_subscriber/ListConcurrentSubscriber.md) + - [ListSubscriber](api/faststream/redis/subscriber/usecases/list_subscriber/ListSubscriber.md) + - stream_subscriber + - [StreamBatchSubscriber](api/faststream/redis/subscriber/usecases/stream_subscriber/StreamBatchSubscriber.md) + - [StreamConcurrentSubscriber](api/faststream/redis/subscriber/usecases/stream_subscriber/StreamConcurrentSubscriber.md) + - [StreamSubscriber](api/faststream/redis/subscriber/usecases/stream_subscriber/StreamSubscriber.md) - testing - [ChannelVisitor](api/faststream/redis/testing/ChannelVisitor.md) - [FakeProducer](api/faststream/redis/testing/FakeProducer.md) @@ -1137,6 +1086,20 @@ search: - [TestRedisBroker](api/faststream/redis/testing/TestRedisBroker.md) - [Visitor](api/faststream/redis/testing/Visitor.md) - [build_message](api/faststream/redis/testing/build_message.md) + - response + - [BatchPublishCommand](api/faststream/response/BatchPublishCommand.md) + - [PublishCommand](api/faststream/response/PublishCommand.md) + - [PublishType](api/faststream/response/PublishType.md) + - [Response](api/faststream/response/Response.md) + - [ensure_response](api/faststream/response/ensure_response.md) + - publish_type + - [PublishType](api/faststream/response/publish_type/PublishType.md) + - response + - [BatchPublishCommand](api/faststream/response/response/BatchPublishCommand.md) + - [PublishCommand](api/faststream/response/response/PublishCommand.md) + - [Response](api/faststream/response/response/Response.md) + - utils + - [ensure_response](api/faststream/response/utils/ensure_response.md) - security - [BaseSecurity](api/faststream/security/BaseSecurity.md) - [SASLGSSAPI](api/faststream/security/SASLGSSAPI.md) @@ -1144,61 +1107,293 @@ search: - [SASLPlaintext](api/faststream/security/SASLPlaintext.md) - [SASLScram256](api/faststream/security/SASLScram256.md) - [SASLScram512](api/faststream/security/SASLScram512.md) - - testing - - [TestApp](api/faststream/testing/TestApp.md) - - app - - [TestApp](api/faststream/testing/app/TestApp.md) - - broker - - [TestBroker](api/faststream/testing/broker/TestBroker.md) - - [patch_broker_calls](api/faststream/testing/broker/patch_broker_calls.md) - - types - - [LoggerProto](api/faststream/types/LoggerProto.md) - - [StandardDataclass](api/faststream/types/StandardDataclass.md) - - utils - - [Context](api/faststream/utils/Context.md) - - [ContextRepo](api/faststream/utils/ContextRepo.md) - - [Depends](api/faststream/utils/Depends.md) - - [Header](api/faststream/utils/Header.md) - - [NoCast](api/faststream/utils/NoCast.md) - - [Path](api/faststream/utils/Path.md) - - [apply_types](api/faststream/utils/apply_types.md) - - ast - - [find_ast_node](api/faststream/utils/ast/find_ast_node.md) - - [find_withitems](api/faststream/utils/ast/find_withitems.md) - - [get_withitem_calls](api/faststream/utils/ast/get_withitem_calls.md) - - [is_contains_context_name](api/faststream/utils/ast/is_contains_context_name.md) - - classes - - [Singleton](api/faststream/utils/classes/Singleton.md) - - context - - [Context](api/faststream/utils/context/Context.md) - - [ContextRepo](api/faststream/utils/context/ContextRepo.md) - - [Header](api/faststream/utils/context/Header.md) - - [Path](api/faststream/utils/context/Path.md) - - builders - - [Context](api/faststream/utils/context/builders/Context.md) - - [Header](api/faststream/utils/context/builders/Header.md) - - [Path](api/faststream/utils/context/builders/Path.md) - - repository - - [ContextRepo](api/faststream/utils/context/repository/ContextRepo.md) - - types - - [Context](api/faststream/utils/context/types/Context.md) - - [resolve_context_by_name](api/faststream/utils/context/types/resolve_context_by_name.md) - - data - - [filter_by_dict](api/faststream/utils/data/filter_by_dict.md) - - functions - - [call_or_await](api/faststream/utils/functions/call_or_await.md) - - [drop_response_type](api/faststream/utils/functions/drop_response_type.md) - - [fake_context](api/faststream/utils/functions/fake_context.md) - - [return_input](api/faststream/utils/functions/return_input.md) - - [sync_fake_context](api/faststream/utils/functions/sync_fake_context.md) - - [timeout_scope](api/faststream/utils/functions/timeout_scope.md) - - [to_async](api/faststream/utils/functions/to_async.md) - - no_cast - - [NoCast](api/faststream/utils/no_cast/NoCast.md) - - nuid - - [NUID](api/faststream/utils/nuid/NUID.md) - - path - - [compile_path](api/faststream/utils/path/compile_path.md) + - specification + - [AsyncAPI](api/faststream/specification/AsyncAPI.md) + - [Contact](api/faststream/specification/Contact.md) + - [ExternalDocs](api/faststream/specification/ExternalDocs.md) + - [License](api/faststream/specification/License.md) + - [Specification](api/faststream/specification/Specification.md) + - [Tag](api/faststream/specification/Tag.md) + - asyncapi + - [AsyncAPI](api/faststream/specification/asyncapi/AsyncAPI.md) + - [get_asyncapi_html](api/faststream/specification/asyncapi/get_asyncapi_html.md) + - factory + - [AsyncAPI](api/faststream/specification/asyncapi/factory/AsyncAPI.md) + - message + - [get_model_schema](api/faststream/specification/asyncapi/message/get_model_schema.md) + - [get_response_schema](api/faststream/specification/asyncapi/message/get_response_schema.md) + - [parse_handler_params](api/faststream/specification/asyncapi/message/parse_handler_params.md) + - site + - [get_asyncapi_html](api/faststream/specification/asyncapi/site/get_asyncapi_html.md) + - [serve_app](api/faststream/specification/asyncapi/site/serve_app.md) + - utils + - [clear_key](api/faststream/specification/asyncapi/utils/clear_key.md) + - [move_pydantic_refs](api/faststream/specification/asyncapi/utils/move_pydantic_refs.md) + - [resolve_payloads](api/faststream/specification/asyncapi/utils/resolve_payloads.md) + - [to_camelcase](api/faststream/specification/asyncapi/utils/to_camelcase.md) + - v2_6_0 + - [AsyncAPI2](api/faststream/specification/asyncapi/v2_6_0/AsyncAPI2.md) + - [get_app_schema](api/faststream/specification/asyncapi/v2_6_0/get_app_schema.md) + - facade + - [AsyncAPI2](api/faststream/specification/asyncapi/v2_6_0/facade/AsyncAPI2.md) + - generate + - [get_app_schema](api/faststream/specification/asyncapi/v2_6_0/generate/get_app_schema.md) + - [get_broker_channels](api/faststream/specification/asyncapi/v2_6_0/generate/get_broker_channels.md) + - [get_broker_server](api/faststream/specification/asyncapi/v2_6_0/generate/get_broker_server.md) + - [resolve_channel_messages](api/faststream/specification/asyncapi/v2_6_0/generate/resolve_channel_messages.md) + - schema + - [ApplicationInfo](api/faststream/specification/asyncapi/v2_6_0/schema/ApplicationInfo.md) + - [ApplicationSchema](api/faststream/specification/asyncapi/v2_6_0/schema/ApplicationSchema.md) + - [Channel](api/faststream/specification/asyncapi/v2_6_0/schema/Channel.md) + - [Components](api/faststream/specification/asyncapi/v2_6_0/schema/Components.md) + - [Contact](api/faststream/specification/asyncapi/v2_6_0/schema/Contact.md) + - [CorrelationId](api/faststream/specification/asyncapi/v2_6_0/schema/CorrelationId.md) + - [ExternalDocs](api/faststream/specification/asyncapi/v2_6_0/schema/ExternalDocs.md) + - [License](api/faststream/specification/asyncapi/v2_6_0/schema/License.md) + - [Message](api/faststream/specification/asyncapi/v2_6_0/schema/Message.md) + - [Operation](api/faststream/specification/asyncapi/v2_6_0/schema/Operation.md) + - [Parameter](api/faststream/specification/asyncapi/v2_6_0/schema/Parameter.md) + - [Reference](api/faststream/specification/asyncapi/v2_6_0/schema/Reference.md) + - [Server](api/faststream/specification/asyncapi/v2_6_0/schema/Server.md) + - [ServerVariable](api/faststream/specification/asyncapi/v2_6_0/schema/ServerVariable.md) + - [Tag](api/faststream/specification/asyncapi/v2_6_0/schema/Tag.md) + - bindings + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/OperationBinding.md) + - amqp + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/OperationBinding.md) + - channel + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/ChannelBinding.md) + - [Exchange](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/Exchange.md) + - [Queue](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/Queue.md) + - operation + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/operation/OperationBinding.md) + - kafka + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/OperationBinding.md) + - channel + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/channel/ChannelBinding.md) + - operation + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/operation/OperationBinding.md) + - main + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/OperationBinding.md) + - channel + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/channel/ChannelBinding.md) + - operation + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/operation/OperationBinding.md) + - nats + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/OperationBinding.md) + - channel + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/channel/ChannelBinding.md) + - operation + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/operation/OperationBinding.md) + - redis + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/OperationBinding.md) + - channel + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/channel/ChannelBinding.md) + - operation + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/operation/OperationBinding.md) + - sqs + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/OperationBinding.md) + - channel + - [ChannelBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/channel/ChannelBinding.md) + - operation + - [OperationBinding](api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/operation/OperationBinding.md) + - channels + - [Channel](api/faststream/specification/asyncapi/v2_6_0/schema/channels/Channel.md) + - components + - [Components](api/faststream/specification/asyncapi/v2_6_0/schema/components/Components.md) + - contact + - [Contact](api/faststream/specification/asyncapi/v2_6_0/schema/contact/Contact.md) + - docs + - [ExternalDocs](api/faststream/specification/asyncapi/v2_6_0/schema/docs/ExternalDocs.md) + - info + - [ApplicationInfo](api/faststream/specification/asyncapi/v2_6_0/schema/info/ApplicationInfo.md) + - license + - [License](api/faststream/specification/asyncapi/v2_6_0/schema/license/License.md) + - message + - [CorrelationId](api/faststream/specification/asyncapi/v2_6_0/schema/message/CorrelationId.md) + - [Message](api/faststream/specification/asyncapi/v2_6_0/schema/message/Message.md) + - operations + - [Operation](api/faststream/specification/asyncapi/v2_6_0/schema/operations/Operation.md) + - schema + - [ApplicationSchema](api/faststream/specification/asyncapi/v2_6_0/schema/schema/ApplicationSchema.md) + - servers + - [Server](api/faststream/specification/asyncapi/v2_6_0/schema/servers/Server.md) + - [ServerVariable](api/faststream/specification/asyncapi/v2_6_0/schema/servers/ServerVariable.md) + - tag + - [Tag](api/faststream/specification/asyncapi/v2_6_0/schema/tag/Tag.md) + - utils + - [Parameter](api/faststream/specification/asyncapi/v2_6_0/schema/utils/Parameter.md) + - [Reference](api/faststream/specification/asyncapi/v2_6_0/schema/utils/Reference.md) + - v3_0_0 + - [AsyncAPI3](api/faststream/specification/asyncapi/v3_0_0/AsyncAPI3.md) + - [get_app_schema](api/faststream/specification/asyncapi/v3_0_0/get_app_schema.md) + - facade + - [AsyncAPI3](api/faststream/specification/asyncapi/v3_0_0/facade/AsyncAPI3.md) + - generate + - [get_app_schema](api/faststream/specification/asyncapi/v3_0_0/generate/get_app_schema.md) + - [get_broker_channels](api/faststream/specification/asyncapi/v3_0_0/generate/get_broker_channels.md) + - [get_broker_server](api/faststream/specification/asyncapi/v3_0_0/generate/get_broker_server.md) + - schema + - [ApplicationInfo](api/faststream/specification/asyncapi/v3_0_0/schema/ApplicationInfo.md) + - [ApplicationSchema](api/faststream/specification/asyncapi/v3_0_0/schema/ApplicationSchema.md) + - [Channel](api/faststream/specification/asyncapi/v3_0_0/schema/Channel.md) + - [Components](api/faststream/specification/asyncapi/v3_0_0/schema/Components.md) + - [Contact](api/faststream/specification/asyncapi/v3_0_0/schema/Contact.md) + - [CorrelationId](api/faststream/specification/asyncapi/v3_0_0/schema/CorrelationId.md) + - [ExternalDocs](api/faststream/specification/asyncapi/v3_0_0/schema/ExternalDocs.md) + - [License](api/faststream/specification/asyncapi/v3_0_0/schema/License.md) + - [Message](api/faststream/specification/asyncapi/v3_0_0/schema/Message.md) + - [Operation](api/faststream/specification/asyncapi/v3_0_0/schema/Operation.md) + - [Parameter](api/faststream/specification/asyncapi/v3_0_0/schema/Parameter.md) + - [Reference](api/faststream/specification/asyncapi/v3_0_0/schema/Reference.md) + - [Server](api/faststream/specification/asyncapi/v3_0_0/schema/Server.md) + - [ServerVariable](api/faststream/specification/asyncapi/v3_0_0/schema/ServerVariable.md) + - [Tag](api/faststream/specification/asyncapi/v3_0_0/schema/Tag.md) + - bindings + - [ChannelBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/OperationBinding.md) + - amqp + - [ChannelBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/OperationBinding.md) + - channel + - [ChannelBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/channel/ChannelBinding.md) + - operation + - [OperationBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/operation/OperationBinding.md) + - kafka + - [ChannelBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/kafka/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/kafka/OperationBinding.md) + - main + - [ChannelBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/OperationBinding.md) + - channel + - [ChannelBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/channel/ChannelBinding.md) + - operation + - [OperationBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/operation/OperationBinding.md) + - nats + - [ChannelBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/nats/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/nats/OperationBinding.md) + - redis + - [ChannelBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/redis/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/redis/OperationBinding.md) + - sqs + - [ChannelBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/sqs/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/asyncapi/v3_0_0/schema/bindings/sqs/OperationBinding.md) + - channels + - [Channel](api/faststream/specification/asyncapi/v3_0_0/schema/channels/Channel.md) + - components + - [Components](api/faststream/specification/asyncapi/v3_0_0/schema/components/Components.md) + - contact + - [Contact](api/faststream/specification/asyncapi/v3_0_0/schema/contact/Contact.md) + - docs + - [ExternalDocs](api/faststream/specification/asyncapi/v3_0_0/schema/docs/ExternalDocs.md) + - info + - [ApplicationInfo](api/faststream/specification/asyncapi/v3_0_0/schema/info/ApplicationInfo.md) + - license + - [License](api/faststream/specification/asyncapi/v3_0_0/schema/license/License.md) + - message + - [CorrelationId](api/faststream/specification/asyncapi/v3_0_0/schema/message/CorrelationId.md) + - [Message](api/faststream/specification/asyncapi/v3_0_0/schema/message/Message.md) + - operations + - [Action](api/faststream/specification/asyncapi/v3_0_0/schema/operations/Action.md) + - [Operation](api/faststream/specification/asyncapi/v3_0_0/schema/operations/Operation.md) + - schema + - [ApplicationSchema](api/faststream/specification/asyncapi/v3_0_0/schema/schema/ApplicationSchema.md) + - servers + - [Server](api/faststream/specification/asyncapi/v3_0_0/schema/servers/Server.md) + - [ServerVariable](api/faststream/specification/asyncapi/v3_0_0/schema/servers/ServerVariable.md) + - tag + - [Tag](api/faststream/specification/asyncapi/v3_0_0/schema/tag/Tag.md) + - utils + - [Parameter](api/faststream/specification/asyncapi/v3_0_0/schema/utils/Parameter.md) + - [Reference](api/faststream/specification/asyncapi/v3_0_0/schema/utils/Reference.md) + - base + - info + - [BaseApplicationInfo](api/faststream/specification/base/info/BaseApplicationInfo.md) + - schema + - [BaseApplicationSchema](api/faststream/specification/base/schema/BaseApplicationSchema.md) + - specification + - [Specification](api/faststream/specification/base/specification/Specification.md) + - schema + - [BrokerSpec](api/faststream/specification/schema/BrokerSpec.md) + - [Contact](api/faststream/specification/schema/Contact.md) + - [ContactDict](api/faststream/specification/schema/ContactDict.md) + - [ExternalDocs](api/faststream/specification/schema/ExternalDocs.md) + - [ExternalDocsDict](api/faststream/specification/schema/ExternalDocsDict.md) + - [License](api/faststream/specification/schema/License.md) + - [LicenseDict](api/faststream/specification/schema/LicenseDict.md) + - [Message](api/faststream/specification/schema/Message.md) + - [Operation](api/faststream/specification/schema/Operation.md) + - [PublisherSpec](api/faststream/specification/schema/PublisherSpec.md) + - [SubscriberSpec](api/faststream/specification/schema/SubscriberSpec.md) + - [Tag](api/faststream/specification/schema/Tag.md) + - [TagDict](api/faststream/specification/schema/TagDict.md) + - bindings + - [ChannelBinding](api/faststream/specification/schema/bindings/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/schema/bindings/OperationBinding.md) + - amqp + - [ChannelBinding](api/faststream/specification/schema/bindings/amqp/ChannelBinding.md) + - [Exchange](api/faststream/specification/schema/bindings/amqp/Exchange.md) + - [OperationBinding](api/faststream/specification/schema/bindings/amqp/OperationBinding.md) + - [Queue](api/faststream/specification/schema/bindings/amqp/Queue.md) + - http + - [OperationBinding](api/faststream/specification/schema/bindings/http/OperationBinding.md) + - kafka + - [ChannelBinding](api/faststream/specification/schema/bindings/kafka/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/schema/bindings/kafka/OperationBinding.md) + - main + - [ChannelBinding](api/faststream/specification/schema/bindings/main/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/schema/bindings/main/OperationBinding.md) + - nats + - [ChannelBinding](api/faststream/specification/schema/bindings/nats/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/schema/bindings/nats/OperationBinding.md) + - redis + - [ChannelBinding](api/faststream/specification/schema/bindings/redis/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/schema/bindings/redis/OperationBinding.md) + - sqs + - [ChannelBinding](api/faststream/specification/schema/bindings/sqs/ChannelBinding.md) + - [OperationBinding](api/faststream/specification/schema/bindings/sqs/OperationBinding.md) + - broker + - [BrokerSpec](api/faststream/specification/schema/broker/BrokerSpec.md) + - extra + - [Contact](api/faststream/specification/schema/extra/Contact.md) + - [ContactDict](api/faststream/specification/schema/extra/ContactDict.md) + - [ExternalDocs](api/faststream/specification/schema/extra/ExternalDocs.md) + - [ExternalDocsDict](api/faststream/specification/schema/extra/ExternalDocsDict.md) + - [License](api/faststream/specification/schema/extra/License.md) + - [LicenseDict](api/faststream/specification/schema/extra/LicenseDict.md) + - [Tag](api/faststream/specification/schema/extra/Tag.md) + - [TagDict](api/faststream/specification/schema/extra/TagDict.md) + - contact + - [Contact](api/faststream/specification/schema/extra/contact/Contact.md) + - [ContactDict](api/faststream/specification/schema/extra/contact/ContactDict.md) + - external_docs + - [ExternalDocs](api/faststream/specification/schema/extra/external_docs/ExternalDocs.md) + - [ExternalDocsDict](api/faststream/specification/schema/extra/external_docs/ExternalDocsDict.md) + - license + - [License](api/faststream/specification/schema/extra/license/License.md) + - [LicenseDict](api/faststream/specification/schema/extra/license/LicenseDict.md) + - tag + - [Tag](api/faststream/specification/schema/extra/tag/Tag.md) + - [TagDict](api/faststream/specification/schema/extra/tag/TagDict.md) + - message + - [Message](api/faststream/specification/schema/message/Message.md) + - model + - [Message](api/faststream/specification/schema/message/model/Message.md) + - operation + - [Operation](api/faststream/specification/schema/operation/Operation.md) + - model + - [Operation](api/faststream/specification/schema/operation/model/Operation.md) + - publisher + - [PublisherSpec](api/faststream/specification/schema/publisher/PublisherSpec.md) + - subscriber + - [SubscriberSpec](api/faststream/specification/schema/subscriber/SubscriberSpec.md) - [FastStream People](faststream-people.md) - Contributing - [Development](getting-started/contributing/CONTRIBUTING.md) diff --git a/docs/docs/en/api/faststream/AckPolicy.md b/docs/docs/en/api/faststream/AckPolicy.md new file mode 100644 index 0000000000..4d7218c81b --- /dev/null +++ b/docs/docs/en/api/faststream/AckPolicy.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.AckPolicy diff --git a/docs/docs/en/api/faststream/app/catch_startup_validation_error.md b/docs/docs/en/api/faststream/app/catch_startup_validation_error.md deleted file mode 100644 index a53e4686f9..0000000000 --- a/docs/docs/en/api/faststream/app/catch_startup_validation_error.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.app.catch_startup_validation_error diff --git a/docs/docs/en/api/faststream/asgi/app/CliRunState.md b/docs/docs/en/api/faststream/asgi/app/CliRunState.md new file mode 100644 index 0000000000..1124f2d50b --- /dev/null +++ b/docs/docs/en/api/faststream/asgi/app/CliRunState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.asgi.app.CliRunState diff --git a/docs/docs/en/api/faststream/asgi/app/OuterRunState.md b/docs/docs/en/api/faststream/asgi/app/OuterRunState.md new file mode 100644 index 0000000000..3789ea0461 --- /dev/null +++ b/docs/docs/en/api/faststream/asgi/app/OuterRunState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.asgi.app.OuterRunState diff --git a/docs/docs/en/api/faststream/asgi/app/ServerState.md b/docs/docs/en/api/faststream/asgi/app/ServerState.md new file mode 100644 index 0000000000..6d5e2c20c6 --- /dev/null +++ b/docs/docs/en/api/faststream/asgi/app/ServerState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.asgi.app.ServerState diff --git a/docs/docs/en/api/faststream/asyncapi/abc/AsyncAPIOperation.md b/docs/docs/en/api/faststream/asyncapi/abc/AsyncAPIOperation.md deleted file mode 100644 index 1e80c37541..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/abc/AsyncAPIOperation.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.abc.AsyncAPIOperation diff --git a/docs/docs/en/api/faststream/asyncapi/generate/get_app_schema.md b/docs/docs/en/api/faststream/asyncapi/generate/get_app_schema.md deleted file mode 100644 index 07475ef5a8..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/generate/get_app_schema.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.generate.get_app_schema diff --git a/docs/docs/en/api/faststream/asyncapi/generate/get_asgi_routes.md b/docs/docs/en/api/faststream/asyncapi/generate/get_asgi_routes.md deleted file mode 100644 index e3d024bd82..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/generate/get_asgi_routes.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.generate.get_asgi_routes diff --git a/docs/docs/en/api/faststream/asyncapi/generate/get_broker_channels.md b/docs/docs/en/api/faststream/asyncapi/generate/get_broker_channels.md deleted file mode 100644 index f5788bae0b..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/generate/get_broker_channels.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.generate.get_broker_channels diff --git a/docs/docs/en/api/faststream/asyncapi/generate/get_broker_server.md b/docs/docs/en/api/faststream/asyncapi/generate/get_broker_server.md deleted file mode 100644 index 5f652d5b59..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/generate/get_broker_server.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.generate.get_broker_server diff --git a/docs/docs/en/api/faststream/asyncapi/get_app_schema.md b/docs/docs/en/api/faststream/asyncapi/get_app_schema.md deleted file mode 100644 index 03d7e4466b..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/get_app_schema.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.get_app_schema diff --git a/docs/docs/en/api/faststream/asyncapi/get_asyncapi_html.md b/docs/docs/en/api/faststream/asyncapi/get_asyncapi_html.md deleted file mode 100644 index 1ed4ce5500..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/get_asyncapi_html.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.get_asyncapi_html diff --git a/docs/docs/en/api/faststream/asyncapi/message/get_model_schema.md b/docs/docs/en/api/faststream/asyncapi/message/get_model_schema.md deleted file mode 100644 index 0099721324..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/message/get_model_schema.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.message.get_model_schema diff --git a/docs/docs/en/api/faststream/asyncapi/message/get_response_schema.md b/docs/docs/en/api/faststream/asyncapi/message/get_response_schema.md deleted file mode 100644 index e297370d01..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/message/get_response_schema.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.message.get_response_schema diff --git a/docs/docs/en/api/faststream/asyncapi/message/parse_handler_params.md b/docs/docs/en/api/faststream/asyncapi/message/parse_handler_params.md deleted file mode 100644 index ffaf1cf7dc..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/message/parse_handler_params.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.message.parse_handler_params diff --git a/docs/docs/en/api/faststream/asyncapi/proto/AsyncAPIApplication.md b/docs/docs/en/api/faststream/asyncapi/proto/AsyncAPIApplication.md deleted file mode 100644 index da1715119d..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/proto/AsyncAPIApplication.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.proto.AsyncAPIApplication diff --git a/docs/docs/en/api/faststream/asyncapi/proto/AsyncAPIProto.md b/docs/docs/en/api/faststream/asyncapi/proto/AsyncAPIProto.md deleted file mode 100644 index 6905c2d82f..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/proto/AsyncAPIProto.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.proto.AsyncAPIProto diff --git a/docs/docs/en/api/faststream/asyncapi/schema/Channel.md b/docs/docs/en/api/faststream/asyncapi/schema/Channel.md deleted file mode 100644 index 4d3b7e83a3..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/Channel.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.Channel diff --git a/docs/docs/en/api/faststream/asyncapi/schema/ChannelBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/ChannelBinding.md deleted file mode 100644 index 4aaf57e584..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/ChannelBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.ChannelBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/Components.md b/docs/docs/en/api/faststream/asyncapi/schema/Components.md deleted file mode 100644 index 9dc785c35e..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/Components.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.Components diff --git a/docs/docs/en/api/faststream/asyncapi/schema/Contact.md b/docs/docs/en/api/faststream/asyncapi/schema/Contact.md deleted file mode 100644 index ded05c314d..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/Contact.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.Contact diff --git a/docs/docs/en/api/faststream/asyncapi/schema/ContactDict.md b/docs/docs/en/api/faststream/asyncapi/schema/ContactDict.md deleted file mode 100644 index 4170e564f6..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/ContactDict.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.ContactDict diff --git a/docs/docs/en/api/faststream/asyncapi/schema/CorrelationId.md b/docs/docs/en/api/faststream/asyncapi/schema/CorrelationId.md deleted file mode 100644 index cd12cdbba6..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/CorrelationId.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.CorrelationId diff --git a/docs/docs/en/api/faststream/asyncapi/schema/ExternalDocs.md b/docs/docs/en/api/faststream/asyncapi/schema/ExternalDocs.md deleted file mode 100644 index 7899164431..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/ExternalDocs.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.ExternalDocs diff --git a/docs/docs/en/api/faststream/asyncapi/schema/ExternalDocsDict.md b/docs/docs/en/api/faststream/asyncapi/schema/ExternalDocsDict.md deleted file mode 100644 index d80a12b10f..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/ExternalDocsDict.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.ExternalDocsDict diff --git a/docs/docs/en/api/faststream/asyncapi/schema/Info.md b/docs/docs/en/api/faststream/asyncapi/schema/Info.md deleted file mode 100644 index 62eb9e4832..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/Info.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.Info diff --git a/docs/docs/en/api/faststream/asyncapi/schema/License.md b/docs/docs/en/api/faststream/asyncapi/schema/License.md deleted file mode 100644 index adb11654e4..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/License.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.License diff --git a/docs/docs/en/api/faststream/asyncapi/schema/LicenseDict.md b/docs/docs/en/api/faststream/asyncapi/schema/LicenseDict.md deleted file mode 100644 index 7c200c4ac7..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/LicenseDict.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.LicenseDict diff --git a/docs/docs/en/api/faststream/asyncapi/schema/Message.md b/docs/docs/en/api/faststream/asyncapi/schema/Message.md deleted file mode 100644 index f04adf939f..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/Message.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.Message diff --git a/docs/docs/en/api/faststream/asyncapi/schema/Operation.md b/docs/docs/en/api/faststream/asyncapi/schema/Operation.md deleted file mode 100644 index 2d43f05b89..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/Operation.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.Operation diff --git a/docs/docs/en/api/faststream/asyncapi/schema/OperationBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/OperationBinding.md deleted file mode 100644 index 0dc2099b66..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/OperationBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.OperationBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/Reference.md b/docs/docs/en/api/faststream/asyncapi/schema/Reference.md deleted file mode 100644 index 778b70e548..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/Reference.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.Reference diff --git a/docs/docs/en/api/faststream/asyncapi/schema/Schema.md b/docs/docs/en/api/faststream/asyncapi/schema/Schema.md deleted file mode 100644 index a496f56769..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/Schema.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.Schema diff --git a/docs/docs/en/api/faststream/asyncapi/schema/SecuritySchemaComponent.md b/docs/docs/en/api/faststream/asyncapi/schema/SecuritySchemaComponent.md deleted file mode 100644 index 61c0a83bf7..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/SecuritySchemaComponent.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.SecuritySchemaComponent diff --git a/docs/docs/en/api/faststream/asyncapi/schema/Server.md b/docs/docs/en/api/faststream/asyncapi/schema/Server.md deleted file mode 100644 index e0d028314e..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/Server.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.Server diff --git a/docs/docs/en/api/faststream/asyncapi/schema/ServerBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/ServerBinding.md deleted file mode 100644 index 8dcaba6701..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/ServerBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.ServerBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/Tag.md b/docs/docs/en/api/faststream/asyncapi/schema/Tag.md deleted file mode 100644 index 0c32584f58..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/Tag.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.Tag diff --git a/docs/docs/en/api/faststream/asyncapi/schema/TagDict.md b/docs/docs/en/api/faststream/asyncapi/schema/TagDict.md deleted file mode 100644 index ebb68351e0..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/TagDict.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.TagDict diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/ChannelBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/ChannelBinding.md deleted file mode 100644 index 51a5ed6586..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/ChannelBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.ChannelBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/OperationBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/OperationBinding.md deleted file mode 100644 index 37a28843be..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/OperationBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.OperationBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/ServerBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/ServerBinding.md deleted file mode 100644 index d91efbfe52..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/ServerBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.ServerBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/ChannelBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/ChannelBinding.md deleted file mode 100644 index 6c5c546126..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/ChannelBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.amqp.ChannelBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/Exchange.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/Exchange.md deleted file mode 100644 index b81a881827..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/Exchange.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.amqp.Exchange diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/OperationBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/OperationBinding.md deleted file mode 100644 index 5b9b34dd78..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/OperationBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.amqp.OperationBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/Queue.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/Queue.md deleted file mode 100644 index 395a7aedb0..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/Queue.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.amqp.Queue diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/ServerBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/ServerBinding.md deleted file mode 100644 index 0daa6510ec..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/amqp/ServerBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.amqp.ServerBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/http/OperationBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/http/OperationBinding.md deleted file mode 100644 index a0251dda0a..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/http/OperationBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.http.OperationBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/kafka/ChannelBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/kafka/ChannelBinding.md deleted file mode 100644 index f327d3147e..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/kafka/ChannelBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.kafka.ChannelBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/kafka/OperationBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/kafka/OperationBinding.md deleted file mode 100644 index adaa645db0..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/kafka/OperationBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.kafka.OperationBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/kafka/ServerBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/kafka/ServerBinding.md deleted file mode 100644 index e52855bd45..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/kafka/ServerBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.kafka.ServerBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/main/ChannelBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/main/ChannelBinding.md deleted file mode 100644 index a2a8872d64..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/main/ChannelBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.main.ChannelBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/main/OperationBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/main/OperationBinding.md deleted file mode 100644 index 1e597b1757..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/main/OperationBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.main.OperationBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/main/ServerBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/main/ServerBinding.md deleted file mode 100644 index 4dacad7825..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/main/ServerBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.main.ServerBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/nats/ChannelBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/nats/ChannelBinding.md deleted file mode 100644 index 11135ad968..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/nats/ChannelBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.nats.ChannelBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/nats/OperationBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/nats/OperationBinding.md deleted file mode 100644 index 8e0cd8acb1..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/nats/OperationBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.nats.OperationBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/nats/ServerBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/nats/ServerBinding.md deleted file mode 100644 index 7d95811c44..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/nats/ServerBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.nats.ServerBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/redis/ChannelBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/redis/ChannelBinding.md deleted file mode 100644 index fef00d4e8a..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/redis/ChannelBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.redis.ChannelBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/redis/OperationBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/redis/OperationBinding.md deleted file mode 100644 index 81b906045b..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/redis/OperationBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.redis.OperationBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/redis/ServerBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/redis/ServerBinding.md deleted file mode 100644 index 7d12316c85..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/redis/ServerBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.redis.ServerBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/sqs/ChannelBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/sqs/ChannelBinding.md deleted file mode 100644 index 4a255559db..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/sqs/ChannelBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.sqs.ChannelBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/sqs/OperationBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/sqs/OperationBinding.md deleted file mode 100644 index 6a438685b4..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/sqs/OperationBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.sqs.OperationBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/bindings/sqs/ServerBinding.md b/docs/docs/en/api/faststream/asyncapi/schema/bindings/sqs/ServerBinding.md deleted file mode 100644 index f6a200b3f7..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/bindings/sqs/ServerBinding.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.bindings.sqs.ServerBinding diff --git a/docs/docs/en/api/faststream/asyncapi/schema/channels/Channel.md b/docs/docs/en/api/faststream/asyncapi/schema/channels/Channel.md deleted file mode 100644 index 7e8a913786..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/channels/Channel.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.channels.Channel diff --git a/docs/docs/en/api/faststream/asyncapi/schema/info/Contact.md b/docs/docs/en/api/faststream/asyncapi/schema/info/Contact.md deleted file mode 100644 index 2dfb0d074e..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/info/Contact.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.info.Contact diff --git a/docs/docs/en/api/faststream/asyncapi/schema/info/ContactDict.md b/docs/docs/en/api/faststream/asyncapi/schema/info/ContactDict.md deleted file mode 100644 index adcd40891f..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/info/ContactDict.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.info.ContactDict diff --git a/docs/docs/en/api/faststream/asyncapi/schema/info/Info.md b/docs/docs/en/api/faststream/asyncapi/schema/info/Info.md deleted file mode 100644 index 88201af129..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/info/Info.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.info.Info diff --git a/docs/docs/en/api/faststream/asyncapi/schema/info/License.md b/docs/docs/en/api/faststream/asyncapi/schema/info/License.md deleted file mode 100644 index ad564b3886..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/info/License.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.info.License diff --git a/docs/docs/en/api/faststream/asyncapi/schema/info/LicenseDict.md b/docs/docs/en/api/faststream/asyncapi/schema/info/LicenseDict.md deleted file mode 100644 index 29fab879e4..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/info/LicenseDict.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.info.LicenseDict diff --git a/docs/docs/en/api/faststream/asyncapi/schema/main/Components.md b/docs/docs/en/api/faststream/asyncapi/schema/main/Components.md deleted file mode 100644 index 782ed0e625..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/main/Components.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.main.Components diff --git a/docs/docs/en/api/faststream/asyncapi/schema/main/Schema.md b/docs/docs/en/api/faststream/asyncapi/schema/main/Schema.md deleted file mode 100644 index 1280877df1..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/main/Schema.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.main.Schema diff --git a/docs/docs/en/api/faststream/asyncapi/schema/message/CorrelationId.md b/docs/docs/en/api/faststream/asyncapi/schema/message/CorrelationId.md deleted file mode 100644 index 7693915525..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/message/CorrelationId.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.message.CorrelationId diff --git a/docs/docs/en/api/faststream/asyncapi/schema/message/Message.md b/docs/docs/en/api/faststream/asyncapi/schema/message/Message.md deleted file mode 100644 index e3959190b0..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/message/Message.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.message.Message diff --git a/docs/docs/en/api/faststream/asyncapi/schema/operations/Operation.md b/docs/docs/en/api/faststream/asyncapi/schema/operations/Operation.md deleted file mode 100644 index 0af1c63cfe..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/operations/Operation.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.operations.Operation diff --git a/docs/docs/en/api/faststream/asyncapi/schema/security/OauthFlowObj.md b/docs/docs/en/api/faststream/asyncapi/schema/security/OauthFlowObj.md deleted file mode 100644 index ea6ad87db9..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/security/OauthFlowObj.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.security.OauthFlowObj diff --git a/docs/docs/en/api/faststream/asyncapi/schema/security/OauthFlows.md b/docs/docs/en/api/faststream/asyncapi/schema/security/OauthFlows.md deleted file mode 100644 index 0c429487fb..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/security/OauthFlows.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.security.OauthFlows diff --git a/docs/docs/en/api/faststream/asyncapi/schema/security/SecuritySchemaComponent.md b/docs/docs/en/api/faststream/asyncapi/schema/security/SecuritySchemaComponent.md deleted file mode 100644 index 779e70fdd6..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/security/SecuritySchemaComponent.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.security.SecuritySchemaComponent diff --git a/docs/docs/en/api/faststream/asyncapi/schema/servers/Server.md b/docs/docs/en/api/faststream/asyncapi/schema/servers/Server.md deleted file mode 100644 index 5af6199d20..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/servers/Server.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.servers.Server diff --git a/docs/docs/en/api/faststream/asyncapi/schema/servers/ServerVariable.md b/docs/docs/en/api/faststream/asyncapi/schema/servers/ServerVariable.md deleted file mode 100644 index 51f99bd3bc..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/servers/ServerVariable.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.servers.ServerVariable diff --git a/docs/docs/en/api/faststream/asyncapi/schema/utils/ExternalDocs.md b/docs/docs/en/api/faststream/asyncapi/schema/utils/ExternalDocs.md deleted file mode 100644 index 207668a5c5..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/utils/ExternalDocs.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.utils.ExternalDocs diff --git a/docs/docs/en/api/faststream/asyncapi/schema/utils/ExternalDocsDict.md b/docs/docs/en/api/faststream/asyncapi/schema/utils/ExternalDocsDict.md deleted file mode 100644 index fc5cedfb73..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/utils/ExternalDocsDict.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.utils.ExternalDocsDict diff --git a/docs/docs/en/api/faststream/asyncapi/schema/utils/Parameter.md b/docs/docs/en/api/faststream/asyncapi/schema/utils/Parameter.md deleted file mode 100644 index 05cc2f3ba3..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/utils/Parameter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.utils.Parameter diff --git a/docs/docs/en/api/faststream/asyncapi/schema/utils/Reference.md b/docs/docs/en/api/faststream/asyncapi/schema/utils/Reference.md deleted file mode 100644 index a47fd931df..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/utils/Reference.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.utils.Reference diff --git a/docs/docs/en/api/faststream/asyncapi/schema/utils/Tag.md b/docs/docs/en/api/faststream/asyncapi/schema/utils/Tag.md deleted file mode 100644 index cf558e756d..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/utils/Tag.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.utils.Tag diff --git a/docs/docs/en/api/faststream/asyncapi/schema/utils/TagDict.md b/docs/docs/en/api/faststream/asyncapi/schema/utils/TagDict.md deleted file mode 100644 index 412546da6f..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/schema/utils/TagDict.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.schema.utils.TagDict diff --git a/docs/docs/en/api/faststream/asyncapi/site/get_asyncapi_html.md b/docs/docs/en/api/faststream/asyncapi/site/get_asyncapi_html.md deleted file mode 100644 index 69af839e6c..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/site/get_asyncapi_html.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.site.get_asyncapi_html diff --git a/docs/docs/en/api/faststream/asyncapi/site/serve_app.md b/docs/docs/en/api/faststream/asyncapi/site/serve_app.md deleted file mode 100644 index c5a1a726e8..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/site/serve_app.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.site.serve_app diff --git a/docs/docs/en/api/faststream/asyncapi/utils/resolve_payloads.md b/docs/docs/en/api/faststream/asyncapi/utils/resolve_payloads.md deleted file mode 100644 index 23aeedd082..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/utils/resolve_payloads.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.utils.resolve_payloads diff --git a/docs/docs/en/api/faststream/asyncapi/utils/to_camelcase.md b/docs/docs/en/api/faststream/asyncapi/utils/to_camelcase.md deleted file mode 100644 index 42cbdf9f29..0000000000 --- a/docs/docs/en/api/faststream/asyncapi/utils/to_camelcase.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.asyncapi.utils.to_camelcase diff --git a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/BaseWatcher.md b/docs/docs/en/api/faststream/broker/acknowledgement_watcher/BaseWatcher.md deleted file mode 100644 index d0c27d17d9..0000000000 --- a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/BaseWatcher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.acknowledgement_watcher.BaseWatcher diff --git a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/CounterWatcher.md b/docs/docs/en/api/faststream/broker/acknowledgement_watcher/CounterWatcher.md deleted file mode 100644 index e299f4c442..0000000000 --- a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/CounterWatcher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.acknowledgement_watcher.CounterWatcher diff --git a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/EndlessWatcher.md b/docs/docs/en/api/faststream/broker/acknowledgement_watcher/EndlessWatcher.md deleted file mode 100644 index b3aac70921..0000000000 --- a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/EndlessWatcher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.acknowledgement_watcher.EndlessWatcher diff --git a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/OneTryWatcher.md b/docs/docs/en/api/faststream/broker/acknowledgement_watcher/OneTryWatcher.md deleted file mode 100644 index 4baa0bdd9c..0000000000 --- a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/OneTryWatcher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.acknowledgement_watcher.OneTryWatcher diff --git a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/WatcherContext.md b/docs/docs/en/api/faststream/broker/acknowledgement_watcher/WatcherContext.md deleted file mode 100644 index ee1ef8643b..0000000000 --- a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/WatcherContext.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.acknowledgement_watcher.WatcherContext diff --git a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/get_watcher.md b/docs/docs/en/api/faststream/broker/acknowledgement_watcher/get_watcher.md deleted file mode 100644 index 9f6869bcf5..0000000000 --- a/docs/docs/en/api/faststream/broker/acknowledgement_watcher/get_watcher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.acknowledgement_watcher.get_watcher diff --git a/docs/docs/en/api/faststream/broker/core/abc/ABCBroker.md b/docs/docs/en/api/faststream/broker/core/abc/ABCBroker.md deleted file mode 100644 index 88b39efd40..0000000000 --- a/docs/docs/en/api/faststream/broker/core/abc/ABCBroker.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.core.abc.ABCBroker diff --git a/docs/docs/en/api/faststream/broker/core/logging/LoggingBroker.md b/docs/docs/en/api/faststream/broker/core/logging/LoggingBroker.md deleted file mode 100644 index b10dd8bc3f..0000000000 --- a/docs/docs/en/api/faststream/broker/core/logging/LoggingBroker.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.core.logging.LoggingBroker diff --git a/docs/docs/en/api/faststream/broker/core/usecase/BrokerUsecase.md b/docs/docs/en/api/faststream/broker/core/usecase/BrokerUsecase.md deleted file mode 100644 index 0e791c5c38..0000000000 --- a/docs/docs/en/api/faststream/broker/core/usecase/BrokerUsecase.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.core.usecase.BrokerUsecase diff --git a/docs/docs/en/api/faststream/broker/fastapi/StreamMessage.md b/docs/docs/en/api/faststream/broker/fastapi/StreamMessage.md deleted file mode 100644 index 2124b279ea..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/StreamMessage.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.StreamMessage diff --git a/docs/docs/en/api/faststream/broker/fastapi/StreamRouter.md b/docs/docs/en/api/faststream/broker/fastapi/StreamRouter.md deleted file mode 100644 index 32a8e8743d..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/StreamRouter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.StreamRouter diff --git a/docs/docs/en/api/faststream/broker/fastapi/config/FastAPIConfig.md b/docs/docs/en/api/faststream/broker/fastapi/config/FastAPIConfig.md deleted file mode 100644 index 685e1b7506..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/config/FastAPIConfig.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.config.FastAPIConfig diff --git a/docs/docs/en/api/faststream/broker/fastapi/context/Context.md b/docs/docs/en/api/faststream/broker/fastapi/context/Context.md deleted file mode 100644 index f4240bb0da..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/context/Context.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.context.Context diff --git a/docs/docs/en/api/faststream/broker/fastapi/get_dependant/get_fastapi_dependant.md b/docs/docs/en/api/faststream/broker/fastapi/get_dependant/get_fastapi_dependant.md deleted file mode 100644 index 1f5d3d1e77..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/get_dependant/get_fastapi_dependant.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.get_dependant.get_fastapi_dependant diff --git a/docs/docs/en/api/faststream/broker/fastapi/get_dependant/get_fastapi_native_dependant.md b/docs/docs/en/api/faststream/broker/fastapi/get_dependant/get_fastapi_native_dependant.md deleted file mode 100644 index f3d6a05e39..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/get_dependant/get_fastapi_native_dependant.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.get_dependant.get_fastapi_native_dependant diff --git a/docs/docs/en/api/faststream/broker/fastapi/get_dependant/has_forbidden_types.md b/docs/docs/en/api/faststream/broker/fastapi/get_dependant/has_forbidden_types.md deleted file mode 100644 index f7052cc628..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/get_dependant/has_forbidden_types.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.get_dependant.has_forbidden_types diff --git a/docs/docs/en/api/faststream/broker/fastapi/get_dependant/is_faststream_decorated.md b/docs/docs/en/api/faststream/broker/fastapi/get_dependant/is_faststream_decorated.md deleted file mode 100644 index ccec2a148a..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/get_dependant/is_faststream_decorated.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.get_dependant.is_faststream_decorated diff --git a/docs/docs/en/api/faststream/broker/fastapi/get_dependant/mark_faststream_decorated.md b/docs/docs/en/api/faststream/broker/fastapi/get_dependant/mark_faststream_decorated.md deleted file mode 100644 index 056f785c8d..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/get_dependant/mark_faststream_decorated.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.get_dependant.mark_faststream_decorated diff --git a/docs/docs/en/api/faststream/broker/fastapi/route/StreamMessage.md b/docs/docs/en/api/faststream/broker/fastapi/route/StreamMessage.md deleted file mode 100644 index 0fbed89be9..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/route/StreamMessage.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.route.StreamMessage diff --git a/docs/docs/en/api/faststream/broker/fastapi/route/build_faststream_to_fastapi_parser.md b/docs/docs/en/api/faststream/broker/fastapi/route/build_faststream_to_fastapi_parser.md deleted file mode 100644 index dc05bb190e..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/route/build_faststream_to_fastapi_parser.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.route.build_faststream_to_fastapi_parser diff --git a/docs/docs/en/api/faststream/broker/fastapi/route/make_fastapi_execution.md b/docs/docs/en/api/faststream/broker/fastapi/route/make_fastapi_execution.md deleted file mode 100644 index f9a0fdd712..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/route/make_fastapi_execution.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.route.make_fastapi_execution diff --git a/docs/docs/en/api/faststream/broker/fastapi/route/wrap_callable_to_fastapi_compatible.md b/docs/docs/en/api/faststream/broker/fastapi/route/wrap_callable_to_fastapi_compatible.md deleted file mode 100644 index ab7081c711..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/route/wrap_callable_to_fastapi_compatible.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.route.wrap_callable_to_fastapi_compatible diff --git a/docs/docs/en/api/faststream/broker/fastapi/router/StreamRouter.md b/docs/docs/en/api/faststream/broker/fastapi/router/StreamRouter.md deleted file mode 100644 index d1f017acc6..0000000000 --- a/docs/docs/en/api/faststream/broker/fastapi/router/StreamRouter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.fastapi.router.StreamRouter diff --git a/docs/docs/en/api/faststream/broker/message/AckStatus.md b/docs/docs/en/api/faststream/broker/message/AckStatus.md deleted file mode 100644 index 412a61de84..0000000000 --- a/docs/docs/en/api/faststream/broker/message/AckStatus.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.message.AckStatus diff --git a/docs/docs/en/api/faststream/broker/message/SourceType.md b/docs/docs/en/api/faststream/broker/message/SourceType.md deleted file mode 100644 index fd242902f9..0000000000 --- a/docs/docs/en/api/faststream/broker/message/SourceType.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.message.SourceType diff --git a/docs/docs/en/api/faststream/broker/message/StreamMessage.md b/docs/docs/en/api/faststream/broker/message/StreamMessage.md deleted file mode 100644 index 800059b91d..0000000000 --- a/docs/docs/en/api/faststream/broker/message/StreamMessage.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.message.StreamMessage diff --git a/docs/docs/en/api/faststream/broker/message/decode_message.md b/docs/docs/en/api/faststream/broker/message/decode_message.md deleted file mode 100644 index a5904b1458..0000000000 --- a/docs/docs/en/api/faststream/broker/message/decode_message.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.message.decode_message diff --git a/docs/docs/en/api/faststream/broker/message/encode_message.md b/docs/docs/en/api/faststream/broker/message/encode_message.md deleted file mode 100644 index ed34f0ceb1..0000000000 --- a/docs/docs/en/api/faststream/broker/message/encode_message.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.message.encode_message diff --git a/docs/docs/en/api/faststream/broker/message/gen_cor_id.md b/docs/docs/en/api/faststream/broker/message/gen_cor_id.md deleted file mode 100644 index 5e4c2a4622..0000000000 --- a/docs/docs/en/api/faststream/broker/message/gen_cor_id.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.message.gen_cor_id diff --git a/docs/docs/en/api/faststream/broker/middlewares/BaseMiddleware.md b/docs/docs/en/api/faststream/broker/middlewares/BaseMiddleware.md deleted file mode 100644 index d81c2fbf20..0000000000 --- a/docs/docs/en/api/faststream/broker/middlewares/BaseMiddleware.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.middlewares.BaseMiddleware diff --git a/docs/docs/en/api/faststream/broker/middlewares/ExceptionMiddleware.md b/docs/docs/en/api/faststream/broker/middlewares/ExceptionMiddleware.md deleted file mode 100644 index 1fa11b80fc..0000000000 --- a/docs/docs/en/api/faststream/broker/middlewares/ExceptionMiddleware.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.middlewares.ExceptionMiddleware diff --git a/docs/docs/en/api/faststream/broker/middlewares/base/BaseMiddleware.md b/docs/docs/en/api/faststream/broker/middlewares/base/BaseMiddleware.md deleted file mode 100644 index 8502288249..0000000000 --- a/docs/docs/en/api/faststream/broker/middlewares/base/BaseMiddleware.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.middlewares.base.BaseMiddleware diff --git a/docs/docs/en/api/faststream/broker/middlewares/exception/BaseExceptionMiddleware.md b/docs/docs/en/api/faststream/broker/middlewares/exception/BaseExceptionMiddleware.md deleted file mode 100644 index 7ab0a414d0..0000000000 --- a/docs/docs/en/api/faststream/broker/middlewares/exception/BaseExceptionMiddleware.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.middlewares.exception.BaseExceptionMiddleware diff --git a/docs/docs/en/api/faststream/broker/middlewares/exception/ExceptionMiddleware.md b/docs/docs/en/api/faststream/broker/middlewares/exception/ExceptionMiddleware.md deleted file mode 100644 index 0abf119ab3..0000000000 --- a/docs/docs/en/api/faststream/broker/middlewares/exception/ExceptionMiddleware.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.middlewares.exception.ExceptionMiddleware diff --git a/docs/docs/en/api/faststream/broker/middlewares/exception/ignore_handler.md b/docs/docs/en/api/faststream/broker/middlewares/exception/ignore_handler.md deleted file mode 100644 index 425561dcba..0000000000 --- a/docs/docs/en/api/faststream/broker/middlewares/exception/ignore_handler.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.middlewares.exception.ignore_handler diff --git a/docs/docs/en/api/faststream/broker/middlewares/logging/CriticalLogMiddleware.md b/docs/docs/en/api/faststream/broker/middlewares/logging/CriticalLogMiddleware.md deleted file mode 100644 index 829368d699..0000000000 --- a/docs/docs/en/api/faststream/broker/middlewares/logging/CriticalLogMiddleware.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.middlewares.logging.CriticalLogMiddleware diff --git a/docs/docs/en/api/faststream/broker/proto/EndpointProto.md b/docs/docs/en/api/faststream/broker/proto/EndpointProto.md deleted file mode 100644 index 5a3b095952..0000000000 --- a/docs/docs/en/api/faststream/broker/proto/EndpointProto.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.proto.EndpointProto diff --git a/docs/docs/en/api/faststream/broker/proto/SetupAble.md b/docs/docs/en/api/faststream/broker/proto/SetupAble.md deleted file mode 100644 index a4b487318e..0000000000 --- a/docs/docs/en/api/faststream/broker/proto/SetupAble.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.proto.SetupAble diff --git a/docs/docs/en/api/faststream/broker/publisher/fake/FakePublisher.md b/docs/docs/en/api/faststream/broker/publisher/fake/FakePublisher.md deleted file mode 100644 index 67b2c04f5c..0000000000 --- a/docs/docs/en/api/faststream/broker/publisher/fake/FakePublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.publisher.fake.FakePublisher diff --git a/docs/docs/en/api/faststream/broker/publisher/proto/BasePublisherProto.md b/docs/docs/en/api/faststream/broker/publisher/proto/BasePublisherProto.md deleted file mode 100644 index ed0944fa14..0000000000 --- a/docs/docs/en/api/faststream/broker/publisher/proto/BasePublisherProto.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.publisher.proto.BasePublisherProto diff --git a/docs/docs/en/api/faststream/broker/publisher/proto/ProducerProto.md b/docs/docs/en/api/faststream/broker/publisher/proto/ProducerProto.md deleted file mode 100644 index 8cf65d4e00..0000000000 --- a/docs/docs/en/api/faststream/broker/publisher/proto/ProducerProto.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.publisher.proto.ProducerProto diff --git a/docs/docs/en/api/faststream/broker/publisher/proto/PublisherProto.md b/docs/docs/en/api/faststream/broker/publisher/proto/PublisherProto.md deleted file mode 100644 index f86760bba6..0000000000 --- a/docs/docs/en/api/faststream/broker/publisher/proto/PublisherProto.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.publisher.proto.PublisherProto diff --git a/docs/docs/en/api/faststream/broker/publisher/usecase/PublisherUsecase.md b/docs/docs/en/api/faststream/broker/publisher/usecase/PublisherUsecase.md deleted file mode 100644 index f1de9539fe..0000000000 --- a/docs/docs/en/api/faststream/broker/publisher/usecase/PublisherUsecase.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.publisher.usecase.PublisherUsecase diff --git a/docs/docs/en/api/faststream/broker/response/Response.md b/docs/docs/en/api/faststream/broker/response/Response.md deleted file mode 100644 index 1163381d7b..0000000000 --- a/docs/docs/en/api/faststream/broker/response/Response.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.response.Response diff --git a/docs/docs/en/api/faststream/broker/response/ensure_response.md b/docs/docs/en/api/faststream/broker/response/ensure_response.md deleted file mode 100644 index b4a98bd4a4..0000000000 --- a/docs/docs/en/api/faststream/broker/response/ensure_response.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.response.ensure_response diff --git a/docs/docs/en/api/faststream/broker/router/ArgsContainer.md b/docs/docs/en/api/faststream/broker/router/ArgsContainer.md deleted file mode 100644 index bd82308c79..0000000000 --- a/docs/docs/en/api/faststream/broker/router/ArgsContainer.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.router.ArgsContainer diff --git a/docs/docs/en/api/faststream/broker/router/BrokerRouter.md b/docs/docs/en/api/faststream/broker/router/BrokerRouter.md deleted file mode 100644 index d6bb82fdd2..0000000000 --- a/docs/docs/en/api/faststream/broker/router/BrokerRouter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.router.BrokerRouter diff --git a/docs/docs/en/api/faststream/broker/router/SubscriberRoute.md b/docs/docs/en/api/faststream/broker/router/SubscriberRoute.md deleted file mode 100644 index 18c3a547ec..0000000000 --- a/docs/docs/en/api/faststream/broker/router/SubscriberRoute.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.router.SubscriberRoute diff --git a/docs/docs/en/api/faststream/broker/schemas/NameRequired.md b/docs/docs/en/api/faststream/broker/schemas/NameRequired.md deleted file mode 100644 index 398f70b421..0000000000 --- a/docs/docs/en/api/faststream/broker/schemas/NameRequired.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.schemas.NameRequired diff --git a/docs/docs/en/api/faststream/broker/subscriber/call_item/HandlerItem.md b/docs/docs/en/api/faststream/broker/subscriber/call_item/HandlerItem.md deleted file mode 100644 index e2f635512c..0000000000 --- a/docs/docs/en/api/faststream/broker/subscriber/call_item/HandlerItem.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.subscriber.call_item.HandlerItem diff --git a/docs/docs/en/api/faststream/broker/subscriber/mixins/ConcurrentMixin.md b/docs/docs/en/api/faststream/broker/subscriber/mixins/ConcurrentMixin.md deleted file mode 100644 index 994f224aea..0000000000 --- a/docs/docs/en/api/faststream/broker/subscriber/mixins/ConcurrentMixin.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.subscriber.mixins.ConcurrentMixin diff --git a/docs/docs/en/api/faststream/broker/subscriber/mixins/TasksMixin.md b/docs/docs/en/api/faststream/broker/subscriber/mixins/TasksMixin.md deleted file mode 100644 index 6d483bef85..0000000000 --- a/docs/docs/en/api/faststream/broker/subscriber/mixins/TasksMixin.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.subscriber.mixins.TasksMixin diff --git a/docs/docs/en/api/faststream/broker/subscriber/proto/SubscriberProto.md b/docs/docs/en/api/faststream/broker/subscriber/proto/SubscriberProto.md deleted file mode 100644 index fd887d41b9..0000000000 --- a/docs/docs/en/api/faststream/broker/subscriber/proto/SubscriberProto.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.subscriber.proto.SubscriberProto diff --git a/docs/docs/en/api/faststream/broker/subscriber/usecase/SubscriberUsecase.md b/docs/docs/en/api/faststream/broker/subscriber/usecase/SubscriberUsecase.md deleted file mode 100644 index f7e9448277..0000000000 --- a/docs/docs/en/api/faststream/broker/subscriber/usecase/SubscriberUsecase.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.subscriber.usecase.SubscriberUsecase diff --git a/docs/docs/en/api/faststream/broker/types/PublisherMiddleware.md b/docs/docs/en/api/faststream/broker/types/PublisherMiddleware.md deleted file mode 100644 index 2c43d2efcb..0000000000 --- a/docs/docs/en/api/faststream/broker/types/PublisherMiddleware.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.types.PublisherMiddleware diff --git a/docs/docs/en/api/faststream/broker/utils/MultiLock.md b/docs/docs/en/api/faststream/broker/utils/MultiLock.md deleted file mode 100644 index 5f4bc6d5cb..0000000000 --- a/docs/docs/en/api/faststream/broker/utils/MultiLock.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.utils.MultiLock diff --git a/docs/docs/en/api/faststream/broker/utils/default_filter.md b/docs/docs/en/api/faststream/broker/utils/default_filter.md deleted file mode 100644 index 3fe25fa14a..0000000000 --- a/docs/docs/en/api/faststream/broker/utils/default_filter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.utils.default_filter diff --git a/docs/docs/en/api/faststream/broker/utils/get_watcher_context.md b/docs/docs/en/api/faststream/broker/utils/get_watcher_context.md deleted file mode 100644 index 883599c043..0000000000 --- a/docs/docs/en/api/faststream/broker/utils/get_watcher_context.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.utils.get_watcher_context diff --git a/docs/docs/en/api/faststream/broker/utils/process_msg.md b/docs/docs/en/api/faststream/broker/utils/process_msg.md deleted file mode 100644 index e7ce8aaf99..0000000000 --- a/docs/docs/en/api/faststream/broker/utils/process_msg.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.utils.process_msg diff --git a/docs/docs/en/api/faststream/broker/utils/resolve_custom_func.md b/docs/docs/en/api/faststream/broker/utils/resolve_custom_func.md deleted file mode 100644 index f72ed3c059..0000000000 --- a/docs/docs/en/api/faststream/broker/utils/resolve_custom_func.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.utils.resolve_custom_func diff --git a/docs/docs/en/api/faststream/broker/wrapper/call/HandlerCallWrapper.md b/docs/docs/en/api/faststream/broker/wrapper/call/HandlerCallWrapper.md deleted file mode 100644 index 4c25733797..0000000000 --- a/docs/docs/en/api/faststream/broker/wrapper/call/HandlerCallWrapper.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.wrapper.call.HandlerCallWrapper diff --git a/docs/docs/en/api/faststream/broker/wrapper/proto/WrapperProto.md b/docs/docs/en/api/faststream/broker/wrapper/proto/WrapperProto.md deleted file mode 100644 index 87ffdf815b..0000000000 --- a/docs/docs/en/api/faststream/broker/wrapper/proto/WrapperProto.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.broker.wrapper.proto.WrapperProto diff --git a/docs/docs/en/api/faststream/cli/docs/app/gen.md b/docs/docs/en/api/faststream/cli/docs/app/gen.md deleted file mode 100644 index 72af7d6688..0000000000 --- a/docs/docs/en/api/faststream/cli/docs/app/gen.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.docs.app.gen diff --git a/docs/docs/en/api/faststream/cli/docs/app/serve.md b/docs/docs/en/api/faststream/cli/docs/app/serve.md deleted file mode 100644 index 3d9ec139d9..0000000000 --- a/docs/docs/en/api/faststream/cli/docs/app/serve.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.docs.app.serve diff --git a/docs/docs/en/api/faststream/cli/main/main.md b/docs/docs/en/api/faststream/cli/main/main.md deleted file mode 100644 index c15cba484c..0000000000 --- a/docs/docs/en/api/faststream/cli/main/main.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.main.main diff --git a/docs/docs/en/api/faststream/cli/main/publish.md b/docs/docs/en/api/faststream/cli/main/publish.md deleted file mode 100644 index 84b490cde8..0000000000 --- a/docs/docs/en/api/faststream/cli/main/publish.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.main.publish diff --git a/docs/docs/en/api/faststream/cli/main/publish_message.md b/docs/docs/en/api/faststream/cli/main/publish_message.md deleted file mode 100644 index a8bb7b8efa..0000000000 --- a/docs/docs/en/api/faststream/cli/main/publish_message.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.main.publish_message diff --git a/docs/docs/en/api/faststream/cli/main/run.md b/docs/docs/en/api/faststream/cli/main/run.md deleted file mode 100644 index 6a01af3d26..0000000000 --- a/docs/docs/en/api/faststream/cli/main/run.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.main.run diff --git a/docs/docs/en/api/faststream/cli/main/version_callback.md b/docs/docs/en/api/faststream/cli/main/version_callback.md deleted file mode 100644 index a5467ffeb7..0000000000 --- a/docs/docs/en/api/faststream/cli/main/version_callback.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.main.version_callback diff --git a/docs/docs/en/api/faststream/cli/supervisors/asgi_multiprocess/ASGIMultiprocess.md b/docs/docs/en/api/faststream/cli/supervisors/asgi_multiprocess/ASGIMultiprocess.md deleted file mode 100644 index 8424b2d5fa..0000000000 --- a/docs/docs/en/api/faststream/cli/supervisors/asgi_multiprocess/ASGIMultiprocess.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.supervisors.asgi_multiprocess.ASGIMultiprocess diff --git a/docs/docs/en/api/faststream/cli/supervisors/asgi_multiprocess/UvicornExtraConfig.md b/docs/docs/en/api/faststream/cli/supervisors/asgi_multiprocess/UvicornExtraConfig.md deleted file mode 100644 index 5e4d3c2831..0000000000 --- a/docs/docs/en/api/faststream/cli/supervisors/asgi_multiprocess/UvicornExtraConfig.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.supervisors.asgi_multiprocess.UvicornExtraConfig diff --git a/docs/docs/en/api/faststream/cli/supervisors/basereload/BaseReload.md b/docs/docs/en/api/faststream/cli/supervisors/basereload/BaseReload.md deleted file mode 100644 index b378b2922a..0000000000 --- a/docs/docs/en/api/faststream/cli/supervisors/basereload/BaseReload.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.supervisors.basereload.BaseReload diff --git a/docs/docs/en/api/faststream/cli/supervisors/multiprocess/Multiprocess.md b/docs/docs/en/api/faststream/cli/supervisors/multiprocess/Multiprocess.md deleted file mode 100644 index 4cdd6d30e3..0000000000 --- a/docs/docs/en/api/faststream/cli/supervisors/multiprocess/Multiprocess.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.supervisors.multiprocess.Multiprocess diff --git a/docs/docs/en/api/faststream/cli/supervisors/utils/get_subprocess.md b/docs/docs/en/api/faststream/cli/supervisors/utils/get_subprocess.md deleted file mode 100644 index 1488078e45..0000000000 --- a/docs/docs/en/api/faststream/cli/supervisors/utils/get_subprocess.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.supervisors.utils.get_subprocess diff --git a/docs/docs/en/api/faststream/cli/supervisors/utils/set_exit.md b/docs/docs/en/api/faststream/cli/supervisors/utils/set_exit.md deleted file mode 100644 index e739d79409..0000000000 --- a/docs/docs/en/api/faststream/cli/supervisors/utils/set_exit.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.supervisors.utils.set_exit diff --git a/docs/docs/en/api/faststream/cli/supervisors/utils/subprocess_started.md b/docs/docs/en/api/faststream/cli/supervisors/utils/subprocess_started.md deleted file mode 100644 index 8840390ca8..0000000000 --- a/docs/docs/en/api/faststream/cli/supervisors/utils/subprocess_started.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.supervisors.utils.subprocess_started diff --git a/docs/docs/en/api/faststream/cli/supervisors/watchfiles/ExtendedFilter.md b/docs/docs/en/api/faststream/cli/supervisors/watchfiles/ExtendedFilter.md deleted file mode 100644 index 095c3cc2f0..0000000000 --- a/docs/docs/en/api/faststream/cli/supervisors/watchfiles/ExtendedFilter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.supervisors.watchfiles.ExtendedFilter diff --git a/docs/docs/en/api/faststream/cli/supervisors/watchfiles/WatchReloader.md b/docs/docs/en/api/faststream/cli/supervisors/watchfiles/WatchReloader.md deleted file mode 100644 index b86533f1e8..0000000000 --- a/docs/docs/en/api/faststream/cli/supervisors/watchfiles/WatchReloader.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.supervisors.watchfiles.WatchReloader diff --git a/docs/docs/en/api/faststream/cli/utils/imports/get_app_path.md b/docs/docs/en/api/faststream/cli/utils/imports/get_app_path.md deleted file mode 100644 index be8fcfef0c..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/imports/get_app_path.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.imports.get_app_path diff --git a/docs/docs/en/api/faststream/cli/utils/imports/import_from_string.md b/docs/docs/en/api/faststream/cli/utils/imports/import_from_string.md deleted file mode 100644 index 731203ac54..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/imports/import_from_string.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.imports.import_from_string diff --git a/docs/docs/en/api/faststream/cli/utils/imports/import_object.md b/docs/docs/en/api/faststream/cli/utils/imports/import_object.md deleted file mode 100644 index e26a3e280c..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/imports/import_object.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.imports.import_object diff --git a/docs/docs/en/api/faststream/cli/utils/imports/try_import_app.md b/docs/docs/en/api/faststream/cli/utils/imports/try_import_app.md deleted file mode 100644 index 0c6df90c86..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/imports/try_import_app.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.imports.try_import_app diff --git a/docs/docs/en/api/faststream/cli/utils/logs/LogFiles.md b/docs/docs/en/api/faststream/cli/utils/logs/LogFiles.md deleted file mode 100644 index 4b32955b46..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/logs/LogFiles.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.logs.LogFiles diff --git a/docs/docs/en/api/faststream/cli/utils/logs/LogLevels.md b/docs/docs/en/api/faststream/cli/utils/logs/LogLevels.md deleted file mode 100644 index f82e3bbb6f..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/logs/LogLevels.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.logs.LogLevels diff --git a/docs/docs/en/api/faststream/cli/utils/logs/get_log_level.md b/docs/docs/en/api/faststream/cli/utils/logs/get_log_level.md deleted file mode 100644 index f5e4fcaea0..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/logs/get_log_level.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.logs.get_log_level diff --git a/docs/docs/en/api/faststream/cli/utils/logs/set_log_config.md b/docs/docs/en/api/faststream/cli/utils/logs/set_log_config.md deleted file mode 100644 index 0ecbc2c4b2..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/logs/set_log_config.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.logs.set_log_config diff --git a/docs/docs/en/api/faststream/cli/utils/logs/set_log_level.md b/docs/docs/en/api/faststream/cli/utils/logs/set_log_level.md deleted file mode 100644 index 6db13adbb9..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/logs/set_log_level.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.logs.set_log_level diff --git a/docs/docs/en/api/faststream/cli/utils/parser/is_bind_arg.md b/docs/docs/en/api/faststream/cli/utils/parser/is_bind_arg.md deleted file mode 100644 index 133a1d5675..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/parser/is_bind_arg.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.parser.is_bind_arg diff --git a/docs/docs/en/api/faststream/cli/utils/parser/parse_cli_args.md b/docs/docs/en/api/faststream/cli/utils/parser/parse_cli_args.md deleted file mode 100644 index 9c6f03d066..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/parser/parse_cli_args.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.parser.parse_cli_args diff --git a/docs/docs/en/api/faststream/cli/utils/parser/remove_prefix.md b/docs/docs/en/api/faststream/cli/utils/parser/remove_prefix.md deleted file mode 100644 index 587db3677f..0000000000 --- a/docs/docs/en/api/faststream/cli/utils/parser/remove_prefix.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.cli.utils.parser.remove_prefix diff --git a/docs/docs/en/api/faststream/confluent/TestApp.md b/docs/docs/en/api/faststream/confluent/TestApp.md index 2468f3755c..ad101303af 100644 --- a/docs/docs/en/api/faststream/confluent/TestApp.md +++ b/docs/docs/en/api/faststream/confluent/TestApp.md @@ -8,4 +8,4 @@ search: boost: 0.5 --- -::: faststream.testing.app.TestApp +::: faststream._internal.testing.app.TestApp diff --git a/docs/docs/en/api/faststream/confluent/broker/KafkaPublisher.md b/docs/docs/en/api/faststream/confluent/broker/KafkaPublisher.md new file mode 100644 index 0000000000..eabc56b08f --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/broker/KafkaPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.broker.KafkaPublisher diff --git a/docs/docs/en/api/faststream/confluent/broker/KafkaRoute.md b/docs/docs/en/api/faststream/confluent/broker/KafkaRoute.md new file mode 100644 index 0000000000..c8b22c23f3 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/broker/KafkaRoute.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.broker.KafkaRoute diff --git a/docs/docs/en/api/faststream/confluent/broker/KafkaRouter.md b/docs/docs/en/api/faststream/confluent/broker/KafkaRouter.md new file mode 100644 index 0000000000..93a4788833 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/broker/KafkaRouter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.broker.KafkaRouter diff --git a/docs/docs/en/api/faststream/confluent/broker/logging/KafkaLoggingBroker.md b/docs/docs/en/api/faststream/confluent/broker/logging/KafkaLoggingBroker.md deleted file mode 100644 index ea238b6b85..0000000000 --- a/docs/docs/en/api/faststream/confluent/broker/logging/KafkaLoggingBroker.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.broker.logging.KafkaLoggingBroker diff --git a/docs/docs/en/api/faststream/confluent/broker/logging/KafkaParamsStorage.md b/docs/docs/en/api/faststream/confluent/broker/logging/KafkaParamsStorage.md new file mode 100644 index 0000000000..900f945037 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/broker/logging/KafkaParamsStorage.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.broker.logging.KafkaParamsStorage diff --git a/docs/docs/en/api/faststream/confluent/broker/router/KafkaPublisher.md b/docs/docs/en/api/faststream/confluent/broker/router/KafkaPublisher.md new file mode 100644 index 0000000000..bddfafd1c3 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/broker/router/KafkaPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.broker.router.KafkaPublisher diff --git a/docs/docs/en/api/faststream/confluent/broker/router/KafkaRoute.md b/docs/docs/en/api/faststream/confluent/broker/router/KafkaRoute.md new file mode 100644 index 0000000000..4d4a2f67a6 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/broker/router/KafkaRoute.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.broker.router.KafkaRoute diff --git a/docs/docs/en/api/faststream/confluent/broker/router/KafkaRouter.md b/docs/docs/en/api/faststream/confluent/broker/router/KafkaRouter.md new file mode 100644 index 0000000000..7b4fb32c74 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/broker/router/KafkaRouter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.broker.router.KafkaRouter diff --git a/docs/docs/en/api/faststream/confluent/client/AsyncConfluentConsumer.md b/docs/docs/en/api/faststream/confluent/client/AsyncConfluentConsumer.md deleted file mode 100644 index 25374c405d..0000000000 --- a/docs/docs/en/api/faststream/confluent/client/AsyncConfluentConsumer.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.client.AsyncConfluentConsumer diff --git a/docs/docs/en/api/faststream/confluent/client/AsyncConfluentProducer.md b/docs/docs/en/api/faststream/confluent/client/AsyncConfluentProducer.md deleted file mode 100644 index 29bfac283f..0000000000 --- a/docs/docs/en/api/faststream/confluent/client/AsyncConfluentProducer.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.client.AsyncConfluentProducer diff --git a/docs/docs/en/api/faststream/confluent/client/BatchBuilder.md b/docs/docs/en/api/faststream/confluent/client/BatchBuilder.md deleted file mode 100644 index 232f9ecdf2..0000000000 --- a/docs/docs/en/api/faststream/confluent/client/BatchBuilder.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.client.BatchBuilder diff --git a/docs/docs/en/api/faststream/confluent/client/check_msg_error.md b/docs/docs/en/api/faststream/confluent/client/check_msg_error.md deleted file mode 100644 index 71ac291b6e..0000000000 --- a/docs/docs/en/api/faststream/confluent/client/check_msg_error.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.client.check_msg_error diff --git a/docs/docs/en/api/faststream/confluent/client/create_topics.md b/docs/docs/en/api/faststream/confluent/client/create_topics.md deleted file mode 100644 index 8efc1a80c4..0000000000 --- a/docs/docs/en/api/faststream/confluent/client/create_topics.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.client.create_topics diff --git a/docs/docs/en/api/faststream/confluent/config/BrokerAddressFamily.md b/docs/docs/en/api/faststream/confluent/config/BrokerAddressFamily.md deleted file mode 100644 index bf5cfbaca7..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/BrokerAddressFamily.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.BrokerAddressFamily diff --git a/docs/docs/en/api/faststream/confluent/config/BuiltinFeatures.md b/docs/docs/en/api/faststream/confluent/config/BuiltinFeatures.md deleted file mode 100644 index 41e324305d..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/BuiltinFeatures.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.BuiltinFeatures diff --git a/docs/docs/en/api/faststream/confluent/config/ClientDNSLookup.md b/docs/docs/en/api/faststream/confluent/config/ClientDNSLookup.md deleted file mode 100644 index 15f67688f1..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/ClientDNSLookup.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.ClientDNSLookup diff --git a/docs/docs/en/api/faststream/confluent/config/CompressionCodec.md b/docs/docs/en/api/faststream/confluent/config/CompressionCodec.md deleted file mode 100644 index dd9640afd4..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/CompressionCodec.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.CompressionCodec diff --git a/docs/docs/en/api/faststream/confluent/config/CompressionType.md b/docs/docs/en/api/faststream/confluent/config/CompressionType.md deleted file mode 100644 index 8139bfcdda..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/CompressionType.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.CompressionType diff --git a/docs/docs/en/api/faststream/confluent/config/ConfluentConfig.md b/docs/docs/en/api/faststream/confluent/config/ConfluentConfig.md deleted file mode 100644 index 9ebd97c1ff..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/ConfluentConfig.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.ConfluentConfig diff --git a/docs/docs/en/api/faststream/confluent/config/ConfluentFastConfig.md b/docs/docs/en/api/faststream/confluent/config/ConfluentFastConfig.md deleted file mode 100644 index 27861ffd5b..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/ConfluentFastConfig.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.ConfluentFastConfig diff --git a/docs/docs/en/api/faststream/confluent/config/Debug.md b/docs/docs/en/api/faststream/confluent/config/Debug.md deleted file mode 100644 index 2036046f5d..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/Debug.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.Debug diff --git a/docs/docs/en/api/faststream/confluent/config/GroupProtocol.md b/docs/docs/en/api/faststream/confluent/config/GroupProtocol.md deleted file mode 100644 index a5cab4b1d9..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/GroupProtocol.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.GroupProtocol diff --git a/docs/docs/en/api/faststream/confluent/config/IsolationLevel.md b/docs/docs/en/api/faststream/confluent/config/IsolationLevel.md deleted file mode 100644 index d122261f0f..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/IsolationLevel.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.IsolationLevel diff --git a/docs/docs/en/api/faststream/confluent/config/OffsetStoreMethod.md b/docs/docs/en/api/faststream/confluent/config/OffsetStoreMethod.md deleted file mode 100644 index 4b203e65e9..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/OffsetStoreMethod.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.OffsetStoreMethod diff --git a/docs/docs/en/api/faststream/confluent/config/SASLOAUTHBearerMethod.md b/docs/docs/en/api/faststream/confluent/config/SASLOAUTHBearerMethod.md deleted file mode 100644 index 2cb635c6b0..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/SASLOAUTHBearerMethod.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.SASLOAUTHBearerMethod diff --git a/docs/docs/en/api/faststream/confluent/config/SecurityProtocol.md b/docs/docs/en/api/faststream/confluent/config/SecurityProtocol.md deleted file mode 100644 index 8415d3214e..0000000000 --- a/docs/docs/en/api/faststream/confluent/config/SecurityProtocol.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.config.SecurityProtocol diff --git a/docs/docs/en/api/faststream/confluent/configs/KafkaBrokerConfig.md b/docs/docs/en/api/faststream/confluent/configs/KafkaBrokerConfig.md new file mode 100644 index 0000000000..f3d34b96e9 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/configs/KafkaBrokerConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.configs.KafkaBrokerConfig diff --git a/docs/docs/en/api/faststream/confluent/configs/broker/ConsumerBuilder.md b/docs/docs/en/api/faststream/confluent/configs/broker/ConsumerBuilder.md new file mode 100644 index 0000000000..e0d792101b --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/configs/broker/ConsumerBuilder.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.configs.broker.ConsumerBuilder diff --git a/docs/docs/en/api/faststream/confluent/configs/broker/KafkaBrokerConfig.md b/docs/docs/en/api/faststream/confluent/configs/broker/KafkaBrokerConfig.md new file mode 100644 index 0000000000..d0d4d286ad --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/configs/broker/KafkaBrokerConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.configs.broker.KafkaBrokerConfig diff --git a/docs/docs/en/api/faststream/confluent/fastapi/Context.md b/docs/docs/en/api/faststream/confluent/fastapi/Context.md index f4240bb0da..99bf141f5c 100644 --- a/docs/docs/en/api/faststream/confluent/fastapi/Context.md +++ b/docs/docs/en/api/faststream/confluent/fastapi/Context.md @@ -8,4 +8,4 @@ search: boost: 0.5 --- -::: faststream.broker.fastapi.context.Context +::: faststream._internal.fastapi.context.Context diff --git a/docs/docs/en/api/faststream/confluent/helpers/AdminService.md b/docs/docs/en/api/faststream/confluent/helpers/AdminService.md new file mode 100644 index 0000000000..ecad2f496b --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/AdminService.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.AdminService diff --git a/docs/docs/en/api/faststream/confluent/helpers/AsyncConfluentConsumer.md b/docs/docs/en/api/faststream/confluent/helpers/AsyncConfluentConsumer.md new file mode 100644 index 0000000000..211a2056a6 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/AsyncConfluentConsumer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.AsyncConfluentConsumer diff --git a/docs/docs/en/api/faststream/confluent/helpers/AsyncConfluentProducer.md b/docs/docs/en/api/faststream/confluent/helpers/AsyncConfluentProducer.md new file mode 100644 index 0000000000..0c931cae17 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/AsyncConfluentProducer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.AsyncConfluentProducer diff --git a/docs/docs/en/api/faststream/confluent/helpers/ConfluentFastConfig.md b/docs/docs/en/api/faststream/confluent/helpers/ConfluentFastConfig.md new file mode 100644 index 0000000000..1a1641fc7b --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/ConfluentFastConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.ConfluentFastConfig diff --git a/docs/docs/en/api/faststream/confluent/helpers/admin/AdminService.md b/docs/docs/en/api/faststream/confluent/helpers/admin/AdminService.md new file mode 100644 index 0000000000..9e5ddc883c --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/admin/AdminService.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.admin.AdminService diff --git a/docs/docs/en/api/faststream/confluent/helpers/admin/CreateResult.md b/docs/docs/en/api/faststream/confluent/helpers/admin/CreateResult.md new file mode 100644 index 0000000000..82f076233c --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/admin/CreateResult.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.admin.CreateResult diff --git a/docs/docs/en/api/faststream/confluent/helpers/client/AsyncConfluentConsumer.md b/docs/docs/en/api/faststream/confluent/helpers/client/AsyncConfluentConsumer.md new file mode 100644 index 0000000000..cbb81ceb02 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/client/AsyncConfluentConsumer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.client.AsyncConfluentConsumer diff --git a/docs/docs/en/api/faststream/confluent/helpers/client/AsyncConfluentProducer.md b/docs/docs/en/api/faststream/confluent/helpers/client/AsyncConfluentProducer.md new file mode 100644 index 0000000000..cd76850ab4 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/client/AsyncConfluentProducer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.client.AsyncConfluentProducer diff --git a/docs/docs/en/api/faststream/confluent/helpers/client/BatchBuilder.md b/docs/docs/en/api/faststream/confluent/helpers/client/BatchBuilder.md new file mode 100644 index 0000000000..02c44fb84b --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/client/BatchBuilder.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.client.BatchBuilder diff --git a/docs/docs/en/api/faststream/confluent/helpers/client/check_msg_error.md b/docs/docs/en/api/faststream/confluent/helpers/client/check_msg_error.md new file mode 100644 index 0000000000..0cf4ad895a --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/client/check_msg_error.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.client.check_msg_error diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/BrokerAddressFamily.md b/docs/docs/en/api/faststream/confluent/helpers/config/BrokerAddressFamily.md new file mode 100644 index 0000000000..221855244a --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/BrokerAddressFamily.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.BrokerAddressFamily diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/BuiltinFeatures.md b/docs/docs/en/api/faststream/confluent/helpers/config/BuiltinFeatures.md new file mode 100644 index 0000000000..043eaafc83 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/BuiltinFeatures.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.BuiltinFeatures diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/ClientDNSLookup.md b/docs/docs/en/api/faststream/confluent/helpers/config/ClientDNSLookup.md new file mode 100644 index 0000000000..22d580b0ab --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/ClientDNSLookup.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.ClientDNSLookup diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/CompressionCodec.md b/docs/docs/en/api/faststream/confluent/helpers/config/CompressionCodec.md new file mode 100644 index 0000000000..8bdba5f219 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/CompressionCodec.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.CompressionCodec diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/CompressionType.md b/docs/docs/en/api/faststream/confluent/helpers/config/CompressionType.md new file mode 100644 index 0000000000..99babf466a --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/CompressionType.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.CompressionType diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/ConfluentConfig.md b/docs/docs/en/api/faststream/confluent/helpers/config/ConfluentConfig.md new file mode 100644 index 0000000000..55cbf67f26 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/ConfluentConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.ConfluentConfig diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/ConfluentFastConfig.md b/docs/docs/en/api/faststream/confluent/helpers/config/ConfluentFastConfig.md new file mode 100644 index 0000000000..8782983a5d --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/ConfluentFastConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.ConfluentFastConfig diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/Debug.md b/docs/docs/en/api/faststream/confluent/helpers/config/Debug.md new file mode 100644 index 0000000000..af9e03ee3d --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/Debug.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.Debug diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/GroupProtocol.md b/docs/docs/en/api/faststream/confluent/helpers/config/GroupProtocol.md new file mode 100644 index 0000000000..5a9f787279 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/GroupProtocol.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.GroupProtocol diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/IsolationLevel.md b/docs/docs/en/api/faststream/confluent/helpers/config/IsolationLevel.md new file mode 100644 index 0000000000..3785a67739 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/IsolationLevel.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.IsolationLevel diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/OffsetStoreMethod.md b/docs/docs/en/api/faststream/confluent/helpers/config/OffsetStoreMethod.md new file mode 100644 index 0000000000..082f1374d8 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/OffsetStoreMethod.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.OffsetStoreMethod diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/SASLOAUTHBearerMethod.md b/docs/docs/en/api/faststream/confluent/helpers/config/SASLOAUTHBearerMethod.md new file mode 100644 index 0000000000..959083900d --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/SASLOAUTHBearerMethod.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.SASLOAUTHBearerMethod diff --git a/docs/docs/en/api/faststream/confluent/helpers/config/SecurityProtocol.md b/docs/docs/en/api/faststream/confluent/helpers/config/SecurityProtocol.md new file mode 100644 index 0000000000..c404d974fa --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/helpers/config/SecurityProtocol.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.helpers.config.SecurityProtocol diff --git a/docs/docs/en/api/faststream/confluent/publisher/asyncapi/AsyncAPIBatchPublisher.md b/docs/docs/en/api/faststream/confluent/publisher/asyncapi/AsyncAPIBatchPublisher.md deleted file mode 100644 index 62ae234697..0000000000 --- a/docs/docs/en/api/faststream/confluent/publisher/asyncapi/AsyncAPIBatchPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.publisher.asyncapi.AsyncAPIBatchPublisher diff --git a/docs/docs/en/api/faststream/confluent/publisher/asyncapi/AsyncAPIDefaultPublisher.md b/docs/docs/en/api/faststream/confluent/publisher/asyncapi/AsyncAPIDefaultPublisher.md deleted file mode 100644 index 32685d612d..0000000000 --- a/docs/docs/en/api/faststream/confluent/publisher/asyncapi/AsyncAPIDefaultPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.publisher.asyncapi.AsyncAPIDefaultPublisher diff --git a/docs/docs/en/api/faststream/confluent/publisher/asyncapi/AsyncAPIPublisher.md b/docs/docs/en/api/faststream/confluent/publisher/asyncapi/AsyncAPIPublisher.md deleted file mode 100644 index f76d27ccd0..0000000000 --- a/docs/docs/en/api/faststream/confluent/publisher/asyncapi/AsyncAPIPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.publisher.asyncapi.AsyncAPIPublisher diff --git a/docs/docs/en/api/faststream/confluent/publisher/config/KafkaPublisherConfig.md b/docs/docs/en/api/faststream/confluent/publisher/config/KafkaPublisherConfig.md new file mode 100644 index 0000000000..ce1d156a72 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/publisher/config/KafkaPublisherConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.publisher.config.KafkaPublisherConfig diff --git a/docs/docs/en/api/faststream/confluent/publisher/config/KafkaPublisherSpecificationConfig.md b/docs/docs/en/api/faststream/confluent/publisher/config/KafkaPublisherSpecificationConfig.md new file mode 100644 index 0000000000..7472316bfc --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/publisher/config/KafkaPublisherSpecificationConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.publisher.config.KafkaPublisherSpecificationConfig diff --git a/docs/docs/en/api/faststream/confluent/publisher/factory/create_publisher.md b/docs/docs/en/api/faststream/confluent/publisher/factory/create_publisher.md new file mode 100644 index 0000000000..60e9664052 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/publisher/factory/create_publisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.publisher.factory.create_publisher diff --git a/docs/docs/en/api/faststream/confluent/publisher/fake/KafkaFakePublisher.md b/docs/docs/en/api/faststream/confluent/publisher/fake/KafkaFakePublisher.md new file mode 100644 index 0000000000..019fbf855f --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/publisher/fake/KafkaFakePublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.publisher.fake.KafkaFakePublisher diff --git a/docs/docs/en/api/faststream/confluent/publisher/producer/AsyncConfluentFastProducerImpl.md b/docs/docs/en/api/faststream/confluent/publisher/producer/AsyncConfluentFastProducerImpl.md new file mode 100644 index 0000000000..af2802fcf7 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/publisher/producer/AsyncConfluentFastProducerImpl.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.publisher.producer.AsyncConfluentFastProducerImpl diff --git a/docs/docs/en/api/faststream/confluent/publisher/producer/FakeConfluentFastProducer.md b/docs/docs/en/api/faststream/confluent/publisher/producer/FakeConfluentFastProducer.md new file mode 100644 index 0000000000..4faa971e89 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/publisher/producer/FakeConfluentFastProducer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.publisher.producer.FakeConfluentFastProducer diff --git a/docs/docs/en/api/faststream/confluent/publisher/specification/KafkaPublisherSpecification.md b/docs/docs/en/api/faststream/confluent/publisher/specification/KafkaPublisherSpecification.md new file mode 100644 index 0000000000..581254f180 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/publisher/specification/KafkaPublisherSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.publisher.specification.KafkaPublisherSpecification diff --git a/docs/docs/en/api/faststream/confluent/publisher/state/EmptyProducerState.md b/docs/docs/en/api/faststream/confluent/publisher/state/EmptyProducerState.md new file mode 100644 index 0000000000..a72476a6d3 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/publisher/state/EmptyProducerState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.publisher.state.EmptyProducerState diff --git a/docs/docs/en/api/faststream/confluent/publisher/state/ProducerState.md b/docs/docs/en/api/faststream/confluent/publisher/state/ProducerState.md new file mode 100644 index 0000000000..5a5a35dddd --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/publisher/state/ProducerState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.publisher.state.ProducerState diff --git a/docs/docs/en/api/faststream/confluent/publisher/state/RealProducer.md b/docs/docs/en/api/faststream/confluent/publisher/state/RealProducer.md new file mode 100644 index 0000000000..52143d1596 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/publisher/state/RealProducer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.publisher.state.RealProducer diff --git a/docs/docs/en/api/faststream/confluent/response/KafkaPublishCommand.md b/docs/docs/en/api/faststream/confluent/response/KafkaPublishCommand.md new file mode 100644 index 0000000000..2a4efcf180 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/response/KafkaPublishCommand.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.response.KafkaPublishCommand diff --git a/docs/docs/en/api/faststream/confluent/router/KafkaPublisher.md b/docs/docs/en/api/faststream/confluent/router/KafkaPublisher.md deleted file mode 100644 index ee1c818707..0000000000 --- a/docs/docs/en/api/faststream/confluent/router/KafkaPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.router.KafkaPublisher diff --git a/docs/docs/en/api/faststream/confluent/router/KafkaRoute.md b/docs/docs/en/api/faststream/confluent/router/KafkaRoute.md deleted file mode 100644 index 60d7bb1c99..0000000000 --- a/docs/docs/en/api/faststream/confluent/router/KafkaRoute.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.router.KafkaRoute diff --git a/docs/docs/en/api/faststream/confluent/router/KafkaRouter.md b/docs/docs/en/api/faststream/confluent/router/KafkaRouter.md deleted file mode 100644 index dac6c1a646..0000000000 --- a/docs/docs/en/api/faststream/confluent/router/KafkaRouter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.router.KafkaRouter diff --git a/docs/docs/en/api/faststream/confluent/schemas/params/ConsumerConnectionParams.md b/docs/docs/en/api/faststream/confluent/schemas/params/ConsumerConnectionParams.md deleted file mode 100644 index f4ed5b2004..0000000000 --- a/docs/docs/en/api/faststream/confluent/schemas/params/ConsumerConnectionParams.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.schemas.params.ConsumerConnectionParams diff --git a/docs/docs/en/api/faststream/confluent/schemas/params/SecurityOptions.md b/docs/docs/en/api/faststream/confluent/schemas/params/SecurityOptions.md deleted file mode 100644 index d8e40ac4ba..0000000000 --- a/docs/docs/en/api/faststream/confluent/schemas/params/SecurityOptions.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.schemas.params.SecurityOptions diff --git a/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPIBatchSubscriber.md b/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPIBatchSubscriber.md deleted file mode 100644 index f6fc81226a..0000000000 --- a/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPIBatchSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.subscriber.asyncapi.AsyncAPIBatchSubscriber diff --git a/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPIConcurrentDefaultSubscriber.md b/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPIConcurrentDefaultSubscriber.md deleted file mode 100644 index 372b29b571..0000000000 --- a/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPIConcurrentDefaultSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.subscriber.asyncapi.AsyncAPIConcurrentDefaultSubscriber diff --git a/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPIDefaultSubscriber.md b/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPIDefaultSubscriber.md deleted file mode 100644 index 12641d32ce..0000000000 --- a/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPIDefaultSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.subscriber.asyncapi.AsyncAPIDefaultSubscriber diff --git a/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPISubscriber.md b/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPISubscriber.md deleted file mode 100644 index b22facc06a..0000000000 --- a/docs/docs/en/api/faststream/confluent/subscriber/asyncapi/AsyncAPISubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.subscriber.asyncapi.AsyncAPISubscriber diff --git a/docs/docs/en/api/faststream/confluent/subscriber/config/KafkaSubscriberConfig.md b/docs/docs/en/api/faststream/confluent/subscriber/config/KafkaSubscriberConfig.md new file mode 100644 index 0000000000..2b65937bc1 --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/subscriber/config/KafkaSubscriberConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.subscriber.config.KafkaSubscriberConfig diff --git a/docs/docs/en/api/faststream/confluent/subscriber/config/KafkaSubscriberSpecificationConfig.md b/docs/docs/en/api/faststream/confluent/subscriber/config/KafkaSubscriberSpecificationConfig.md new file mode 100644 index 0000000000..cbe184d92b --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/subscriber/config/KafkaSubscriberSpecificationConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.subscriber.config.KafkaSubscriberSpecificationConfig diff --git a/docs/docs/en/api/faststream/confluent/subscriber/factory/create_publisher.md b/docs/docs/en/api/faststream/confluent/subscriber/factory/create_publisher.md deleted file mode 100644 index b76699fdad..0000000000 --- a/docs/docs/en/api/faststream/confluent/subscriber/factory/create_publisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.confluent.subscriber.factory.create_publisher diff --git a/docs/docs/en/api/faststream/confluent/subscriber/specification/KafkaSubscriberSpecification.md b/docs/docs/en/api/faststream/confluent/subscriber/specification/KafkaSubscriberSpecification.md new file mode 100644 index 0000000000..ae12113eed --- /dev/null +++ b/docs/docs/en/api/faststream/confluent/subscriber/specification/KafkaSubscriberSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.confluent.subscriber.specification.KafkaSubscriberSpecification diff --git a/docs/docs/en/api/faststream/constants/ContentTypes.md b/docs/docs/en/api/faststream/constants/ContentTypes.md deleted file mode 100644 index 28d62bdcd7..0000000000 --- a/docs/docs/en/api/faststream/constants/ContentTypes.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.constants.ContentTypes diff --git a/docs/docs/en/api/faststream/exceptions/ContextError.md b/docs/docs/en/api/faststream/exceptions/ContextError.md new file mode 100644 index 0000000000..73b4fcdd21 --- /dev/null +++ b/docs/docs/en/api/faststream/exceptions/ContextError.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.exceptions.ContextError diff --git a/docs/docs/en/api/faststream/exceptions/FeatureNotSupportedException.md b/docs/docs/en/api/faststream/exceptions/FeatureNotSupportedException.md new file mode 100644 index 0000000000..bbf1f32d2b --- /dev/null +++ b/docs/docs/en/api/faststream/exceptions/FeatureNotSupportedException.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.exceptions.FeatureNotSupportedException diff --git a/docs/docs/en/api/faststream/exceptions/IncorrectState.md b/docs/docs/en/api/faststream/exceptions/IncorrectState.md new file mode 100644 index 0000000000..2c890d5358 --- /dev/null +++ b/docs/docs/en/api/faststream/exceptions/IncorrectState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.exceptions.IncorrectState diff --git a/docs/docs/en/api/faststream/exceptions/OperationForbiddenError.md b/docs/docs/en/api/faststream/exceptions/OperationForbiddenError.md deleted file mode 100644 index e34e86542b..0000000000 --- a/docs/docs/en/api/faststream/exceptions/OperationForbiddenError.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.exceptions.OperationForbiddenError diff --git a/docs/docs/en/api/faststream/exceptions/StartupValidationError.md b/docs/docs/en/api/faststream/exceptions/StartupValidationError.md new file mode 100644 index 0000000000..05b8e7b74d --- /dev/null +++ b/docs/docs/en/api/faststream/exceptions/StartupValidationError.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.exceptions.StartupValidationError diff --git a/docs/docs/en/api/faststream/exceptions/ValidationError.md b/docs/docs/en/api/faststream/exceptions/ValidationError.md deleted file mode 100644 index 93dc0a73d1..0000000000 --- a/docs/docs/en/api/faststream/exceptions/ValidationError.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.exceptions.ValidationError diff --git a/docs/docs/en/api/faststream/kafka/TestApp.md b/docs/docs/en/api/faststream/kafka/TestApp.md index 2468f3755c..ad101303af 100644 --- a/docs/docs/en/api/faststream/kafka/TestApp.md +++ b/docs/docs/en/api/faststream/kafka/TestApp.md @@ -8,4 +8,4 @@ search: boost: 0.5 --- -::: faststream.testing.app.TestApp +::: faststream._internal.testing.app.TestApp diff --git a/docs/docs/en/api/faststream/kafka/broker/KafkaPublisher.md b/docs/docs/en/api/faststream/kafka/broker/KafkaPublisher.md new file mode 100644 index 0000000000..9ee6ea6783 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/broker/KafkaPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.broker.KafkaPublisher diff --git a/docs/docs/en/api/faststream/kafka/broker/KafkaRoute.md b/docs/docs/en/api/faststream/kafka/broker/KafkaRoute.md new file mode 100644 index 0000000000..9983739c87 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/broker/KafkaRoute.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.broker.KafkaRoute diff --git a/docs/docs/en/api/faststream/kafka/broker/KafkaRouter.md b/docs/docs/en/api/faststream/kafka/broker/KafkaRouter.md new file mode 100644 index 0000000000..0de01bceb9 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/broker/KafkaRouter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.broker.KafkaRouter diff --git a/docs/docs/en/api/faststream/kafka/broker/logging/KafkaLoggingBroker.md b/docs/docs/en/api/faststream/kafka/broker/logging/KafkaLoggingBroker.md deleted file mode 100644 index 1f8d5921b7..0000000000 --- a/docs/docs/en/api/faststream/kafka/broker/logging/KafkaLoggingBroker.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.broker.logging.KafkaLoggingBroker diff --git a/docs/docs/en/api/faststream/kafka/broker/logging/KafkaParamsStorage.md b/docs/docs/en/api/faststream/kafka/broker/logging/KafkaParamsStorage.md new file mode 100644 index 0000000000..f7c8136115 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/broker/logging/KafkaParamsStorage.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.broker.logging.KafkaParamsStorage diff --git a/docs/docs/en/api/faststream/kafka/broker/router/KafkaPublisher.md b/docs/docs/en/api/faststream/kafka/broker/router/KafkaPublisher.md new file mode 100644 index 0000000000..d0c0c65e0c --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/broker/router/KafkaPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.broker.router.KafkaPublisher diff --git a/docs/docs/en/api/faststream/kafka/broker/router/KafkaRoute.md b/docs/docs/en/api/faststream/kafka/broker/router/KafkaRoute.md new file mode 100644 index 0000000000..8ec2511c66 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/broker/router/KafkaRoute.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.broker.router.KafkaRoute diff --git a/docs/docs/en/api/faststream/kafka/broker/router/KafkaRouter.md b/docs/docs/en/api/faststream/kafka/broker/router/KafkaRouter.md new file mode 100644 index 0000000000..a4eeac4fe9 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/broker/router/KafkaRouter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.broker.router.KafkaRouter diff --git a/docs/docs/en/api/faststream/kafka/configs/KafkaBrokerConfig.md b/docs/docs/en/api/faststream/kafka/configs/KafkaBrokerConfig.md new file mode 100644 index 0000000000..fcff4b7697 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/configs/KafkaBrokerConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.configs.KafkaBrokerConfig diff --git a/docs/docs/en/api/faststream/kafka/configs/broker/KafkaBrokerConfig.md b/docs/docs/en/api/faststream/kafka/configs/broker/KafkaBrokerConfig.md new file mode 100644 index 0000000000..80c8e2abde --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/configs/broker/KafkaBrokerConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.configs.broker.KafkaBrokerConfig diff --git a/docs/docs/en/api/faststream/kafka/fastapi/Context.md b/docs/docs/en/api/faststream/kafka/fastapi/Context.md index f4240bb0da..99bf141f5c 100644 --- a/docs/docs/en/api/faststream/kafka/fastapi/Context.md +++ b/docs/docs/en/api/faststream/kafka/fastapi/Context.md @@ -8,4 +8,4 @@ search: boost: 0.5 --- -::: faststream.broker.fastapi.context.Context +::: faststream._internal.fastapi.context.Context diff --git a/docs/docs/en/api/faststream/kafka/helpers/make_logging_listener.md b/docs/docs/en/api/faststream/kafka/helpers/make_logging_listener.md new file mode 100644 index 0000000000..b64653de4c --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/helpers/make_logging_listener.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.helpers.make_logging_listener diff --git a/docs/docs/en/api/faststream/kafka/helpers/rebalance_listener/make_logging_listener.md b/docs/docs/en/api/faststream/kafka/helpers/rebalance_listener/make_logging_listener.md new file mode 100644 index 0000000000..bf507e1222 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/helpers/rebalance_listener/make_logging_listener.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.helpers.rebalance_listener.make_logging_listener diff --git a/docs/docs/en/api/faststream/kafka/listener/LoggingListenerProxy.md b/docs/docs/en/api/faststream/kafka/listener/LoggingListenerProxy.md deleted file mode 100644 index 11036cd0e1..0000000000 --- a/docs/docs/en/api/faststream/kafka/listener/LoggingListenerProxy.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.listener.LoggingListenerProxy diff --git a/docs/docs/en/api/faststream/kafka/listener/make_logging_listener.md b/docs/docs/en/api/faststream/kafka/listener/make_logging_listener.md new file mode 100644 index 0000000000..bb8eb41818 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/listener/make_logging_listener.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.listener.make_logging_listener diff --git a/docs/docs/en/api/faststream/kafka/publisher/asyncapi/AsyncAPIBatchPublisher.md b/docs/docs/en/api/faststream/kafka/publisher/asyncapi/AsyncAPIBatchPublisher.md deleted file mode 100644 index 8d796523e6..0000000000 --- a/docs/docs/en/api/faststream/kafka/publisher/asyncapi/AsyncAPIBatchPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.publisher.asyncapi.AsyncAPIBatchPublisher diff --git a/docs/docs/en/api/faststream/kafka/publisher/asyncapi/AsyncAPIDefaultPublisher.md b/docs/docs/en/api/faststream/kafka/publisher/asyncapi/AsyncAPIDefaultPublisher.md deleted file mode 100644 index 7e4e54d030..0000000000 --- a/docs/docs/en/api/faststream/kafka/publisher/asyncapi/AsyncAPIDefaultPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.publisher.asyncapi.AsyncAPIDefaultPublisher diff --git a/docs/docs/en/api/faststream/kafka/publisher/asyncapi/AsyncAPIPublisher.md b/docs/docs/en/api/faststream/kafka/publisher/asyncapi/AsyncAPIPublisher.md deleted file mode 100644 index 7d914809c2..0000000000 --- a/docs/docs/en/api/faststream/kafka/publisher/asyncapi/AsyncAPIPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.publisher.asyncapi.AsyncAPIPublisher diff --git a/docs/docs/en/api/faststream/kafka/publisher/config/KafkaPublisherConfig.md b/docs/docs/en/api/faststream/kafka/publisher/config/KafkaPublisherConfig.md new file mode 100644 index 0000000000..0b9e59d4ba --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/publisher/config/KafkaPublisherConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.publisher.config.KafkaPublisherConfig diff --git a/docs/docs/en/api/faststream/kafka/publisher/config/KafkaPublisherSpecificationConfig.md b/docs/docs/en/api/faststream/kafka/publisher/config/KafkaPublisherSpecificationConfig.md new file mode 100644 index 0000000000..cc140385bf --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/publisher/config/KafkaPublisherSpecificationConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.publisher.config.KafkaPublisherSpecificationConfig diff --git a/docs/docs/en/api/faststream/kafka/publisher/factory/create_publisher.md b/docs/docs/en/api/faststream/kafka/publisher/factory/create_publisher.md new file mode 100644 index 0000000000..7ec33758af --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/publisher/factory/create_publisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.publisher.factory.create_publisher diff --git a/docs/docs/en/api/faststream/kafka/publisher/fake/KafkaFakePublisher.md b/docs/docs/en/api/faststream/kafka/publisher/fake/KafkaFakePublisher.md new file mode 100644 index 0000000000..6bacca904e --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/publisher/fake/KafkaFakePublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.publisher.fake.KafkaFakePublisher diff --git a/docs/docs/en/api/faststream/kafka/publisher/producer/AioKafkaFastProducerImpl.md b/docs/docs/en/api/faststream/kafka/publisher/producer/AioKafkaFastProducerImpl.md new file mode 100644 index 0000000000..d619c136ef --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/publisher/producer/AioKafkaFastProducerImpl.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.publisher.producer.AioKafkaFastProducerImpl diff --git a/docs/docs/en/api/faststream/kafka/publisher/producer/FakeAioKafkaFastProducer.md b/docs/docs/en/api/faststream/kafka/publisher/producer/FakeAioKafkaFastProducer.md new file mode 100644 index 0000000000..4700d073df --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/publisher/producer/FakeAioKafkaFastProducer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.publisher.producer.FakeAioKafkaFastProducer diff --git a/docs/docs/en/api/faststream/kafka/publisher/specification/KafkaPublisherSpecification.md b/docs/docs/en/api/faststream/kafka/publisher/specification/KafkaPublisherSpecification.md new file mode 100644 index 0000000000..4fbe2513fa --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/publisher/specification/KafkaPublisherSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.publisher.specification.KafkaPublisherSpecification diff --git a/docs/docs/en/api/faststream/kafka/publisher/state/EmptyProducerState.md b/docs/docs/en/api/faststream/kafka/publisher/state/EmptyProducerState.md new file mode 100644 index 0000000000..0152ee7c2f --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/publisher/state/EmptyProducerState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.publisher.state.EmptyProducerState diff --git a/docs/docs/en/api/faststream/kafka/publisher/state/ProducerState.md b/docs/docs/en/api/faststream/kafka/publisher/state/ProducerState.md new file mode 100644 index 0000000000..c937179471 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/publisher/state/ProducerState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.publisher.state.ProducerState diff --git a/docs/docs/en/api/faststream/kafka/publisher/state/RealProducer.md b/docs/docs/en/api/faststream/kafka/publisher/state/RealProducer.md new file mode 100644 index 0000000000..a576226b3c --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/publisher/state/RealProducer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.publisher.state.RealProducer diff --git a/docs/docs/en/api/faststream/kafka/response/KafkaPublishCommand.md b/docs/docs/en/api/faststream/kafka/response/KafkaPublishCommand.md new file mode 100644 index 0000000000..4852098fcc --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/response/KafkaPublishCommand.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.response.KafkaPublishCommand diff --git a/docs/docs/en/api/faststream/kafka/router/KafkaPublisher.md b/docs/docs/en/api/faststream/kafka/router/KafkaPublisher.md deleted file mode 100644 index 5027c18f20..0000000000 --- a/docs/docs/en/api/faststream/kafka/router/KafkaPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.router.KafkaPublisher diff --git a/docs/docs/en/api/faststream/kafka/router/KafkaRoute.md b/docs/docs/en/api/faststream/kafka/router/KafkaRoute.md deleted file mode 100644 index e7e6184deb..0000000000 --- a/docs/docs/en/api/faststream/kafka/router/KafkaRoute.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.router.KafkaRoute diff --git a/docs/docs/en/api/faststream/kafka/router/KafkaRouter.md b/docs/docs/en/api/faststream/kafka/router/KafkaRouter.md deleted file mode 100644 index 5d7578bbfc..0000000000 --- a/docs/docs/en/api/faststream/kafka/router/KafkaRouter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.router.KafkaRouter diff --git a/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIBatchSubscriber.md b/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIBatchSubscriber.md deleted file mode 100644 index 3ce948d2e2..0000000000 --- a/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIBatchSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.subscriber.asyncapi.AsyncAPIBatchSubscriber diff --git a/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIConcurrentBetweenPartitionsSubscriber.md b/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIConcurrentBetweenPartitionsSubscriber.md deleted file mode 100644 index b64722f460..0000000000 --- a/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIConcurrentBetweenPartitionsSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.subscriber.asyncapi.AsyncAPIConcurrentBetweenPartitionsSubscriber diff --git a/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIConcurrentDefaultSubscriber.md b/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIConcurrentDefaultSubscriber.md deleted file mode 100644 index 8ce5838961..0000000000 --- a/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIConcurrentDefaultSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.subscriber.asyncapi.AsyncAPIConcurrentDefaultSubscriber diff --git a/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIDefaultSubscriber.md b/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIDefaultSubscriber.md deleted file mode 100644 index ef10b05e80..0000000000 --- a/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPIDefaultSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.subscriber.asyncapi.AsyncAPIDefaultSubscriber diff --git a/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPISubscriber.md b/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPISubscriber.md deleted file mode 100644 index 330a621bf5..0000000000 --- a/docs/docs/en/api/faststream/kafka/subscriber/asyncapi/AsyncAPISubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.subscriber.asyncapi.AsyncAPISubscriber diff --git a/docs/docs/en/api/faststream/kafka/subscriber/config/KafkaSubscriberConfig.md b/docs/docs/en/api/faststream/kafka/subscriber/config/KafkaSubscriberConfig.md new file mode 100644 index 0000000000..eb3f32d053 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/subscriber/config/KafkaSubscriberConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.subscriber.config.KafkaSubscriberConfig diff --git a/docs/docs/en/api/faststream/kafka/subscriber/config/KafkaSubscriberSpecificationConfig.md b/docs/docs/en/api/faststream/kafka/subscriber/config/KafkaSubscriberSpecificationConfig.md new file mode 100644 index 0000000000..088b3895c7 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/subscriber/config/KafkaSubscriberSpecificationConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.subscriber.config.KafkaSubscriberSpecificationConfig diff --git a/docs/docs/en/api/faststream/kafka/subscriber/factory/create_publisher.md b/docs/docs/en/api/faststream/kafka/subscriber/factory/create_publisher.md deleted file mode 100644 index 05e3eede8f..0000000000 --- a/docs/docs/en/api/faststream/kafka/subscriber/factory/create_publisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.kafka.subscriber.factory.create_publisher diff --git a/docs/docs/en/api/faststream/kafka/subscriber/specification/KafkaSubscriberSpecification.md b/docs/docs/en/api/faststream/kafka/subscriber/specification/KafkaSubscriberSpecification.md new file mode 100644 index 0000000000..f12aea4e9c --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/subscriber/specification/KafkaSubscriberSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.subscriber.specification.KafkaSubscriberSpecification diff --git a/docs/docs/en/api/faststream/kafka/testing/FakeConsumer.md b/docs/docs/en/api/faststream/kafka/testing/FakeConsumer.md new file mode 100644 index 0000000000..8f10ed9f71 --- /dev/null +++ b/docs/docs/en/api/faststream/kafka/testing/FakeConsumer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.kafka.testing.FakeConsumer diff --git a/docs/docs/en/api/faststream/log/formatter/ColourizedFormatter.md b/docs/docs/en/api/faststream/log/formatter/ColourizedFormatter.md deleted file mode 100644 index 6e1aec157c..0000000000 --- a/docs/docs/en/api/faststream/log/formatter/ColourizedFormatter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.log.formatter.ColourizedFormatter diff --git a/docs/docs/en/api/faststream/log/formatter/expand_log_field.md b/docs/docs/en/api/faststream/log/formatter/expand_log_field.md deleted file mode 100644 index ce943209af..0000000000 --- a/docs/docs/en/api/faststream/log/formatter/expand_log_field.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.log.formatter.expand_log_field diff --git a/docs/docs/en/api/faststream/log/logging/ExtendedFilter.md b/docs/docs/en/api/faststream/log/logging/ExtendedFilter.md deleted file mode 100644 index bd8f017947..0000000000 --- a/docs/docs/en/api/faststream/log/logging/ExtendedFilter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.log.logging.ExtendedFilter diff --git a/docs/docs/en/api/faststream/log/logging/get_broker_logger.md b/docs/docs/en/api/faststream/log/logging/get_broker_logger.md deleted file mode 100644 index e3433fc8dd..0000000000 --- a/docs/docs/en/api/faststream/log/logging/get_broker_logger.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.log.logging.get_broker_logger diff --git a/docs/docs/en/api/faststream/log/logging/set_logger_fmt.md b/docs/docs/en/api/faststream/log/logging/set_logger_fmt.md deleted file mode 100644 index a4af3d137f..0000000000 --- a/docs/docs/en/api/faststream/log/logging/set_logger_fmt.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.log.logging.set_logger_fmt diff --git a/docs/docs/en/api/faststream/message/AckStatus.md b/docs/docs/en/api/faststream/message/AckStatus.md new file mode 100644 index 0000000000..b8d3d6c6c8 --- /dev/null +++ b/docs/docs/en/api/faststream/message/AckStatus.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.AckStatus diff --git a/docs/docs/en/api/faststream/message/SourceType.md b/docs/docs/en/api/faststream/message/SourceType.md new file mode 100644 index 0000000000..7df391eac3 --- /dev/null +++ b/docs/docs/en/api/faststream/message/SourceType.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.SourceType diff --git a/docs/docs/en/api/faststream/message/StreamMessage.md b/docs/docs/en/api/faststream/message/StreamMessage.md new file mode 100644 index 0000000000..5f072b2410 --- /dev/null +++ b/docs/docs/en/api/faststream/message/StreamMessage.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.StreamMessage diff --git a/docs/docs/en/api/faststream/message/decode_message.md b/docs/docs/en/api/faststream/message/decode_message.md new file mode 100644 index 0000000000..c0dce11670 --- /dev/null +++ b/docs/docs/en/api/faststream/message/decode_message.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.decode_message diff --git a/docs/docs/en/api/faststream/message/encode_message.md b/docs/docs/en/api/faststream/message/encode_message.md new file mode 100644 index 0000000000..7d33d8d904 --- /dev/null +++ b/docs/docs/en/api/faststream/message/encode_message.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.encode_message diff --git a/docs/docs/en/api/faststream/message/gen_cor_id.md b/docs/docs/en/api/faststream/message/gen_cor_id.md new file mode 100644 index 0000000000..0abdf298b9 --- /dev/null +++ b/docs/docs/en/api/faststream/message/gen_cor_id.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.gen_cor_id diff --git a/docs/docs/en/api/faststream/message/message/AckStatus.md b/docs/docs/en/api/faststream/message/message/AckStatus.md new file mode 100644 index 0000000000..80940a8ba7 --- /dev/null +++ b/docs/docs/en/api/faststream/message/message/AckStatus.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.message.AckStatus diff --git a/docs/docs/en/api/faststream/message/message/StreamMessage.md b/docs/docs/en/api/faststream/message/message/StreamMessage.md new file mode 100644 index 0000000000..a41232b74c --- /dev/null +++ b/docs/docs/en/api/faststream/message/message/StreamMessage.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.message.StreamMessage diff --git a/docs/docs/en/api/faststream/message/source_type/SourceType.md b/docs/docs/en/api/faststream/message/source_type/SourceType.md new file mode 100644 index 0000000000..8a6fc990e4 --- /dev/null +++ b/docs/docs/en/api/faststream/message/source_type/SourceType.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.source_type.SourceType diff --git a/docs/docs/en/api/faststream/message/utils/decode_message.md b/docs/docs/en/api/faststream/message/utils/decode_message.md new file mode 100644 index 0000000000..b2ec48dac0 --- /dev/null +++ b/docs/docs/en/api/faststream/message/utils/decode_message.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.utils.decode_message diff --git a/docs/docs/en/api/faststream/message/utils/encode_message.md b/docs/docs/en/api/faststream/message/utils/encode_message.md new file mode 100644 index 0000000000..7401e07da5 --- /dev/null +++ b/docs/docs/en/api/faststream/message/utils/encode_message.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.utils.encode_message diff --git a/docs/docs/en/api/faststream/message/utils/gen_cor_id.md b/docs/docs/en/api/faststream/message/utils/gen_cor_id.md new file mode 100644 index 0000000000..74b49c30d2 --- /dev/null +++ b/docs/docs/en/api/faststream/message/utils/gen_cor_id.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.message.utils.gen_cor_id diff --git a/docs/docs/en/api/faststream/middlewares/AckPolicy.md b/docs/docs/en/api/faststream/middlewares/AckPolicy.md new file mode 100644 index 0000000000..82d0033dfb --- /dev/null +++ b/docs/docs/en/api/faststream/middlewares/AckPolicy.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.middlewares.AckPolicy diff --git a/docs/docs/en/api/faststream/middlewares/AcknowledgementMiddleware.md b/docs/docs/en/api/faststream/middlewares/AcknowledgementMiddleware.md new file mode 100644 index 0000000000..d3e7d6a763 --- /dev/null +++ b/docs/docs/en/api/faststream/middlewares/AcknowledgementMiddleware.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.middlewares.AcknowledgementMiddleware diff --git a/docs/docs/en/api/faststream/middlewares/BaseMiddleware.md b/docs/docs/en/api/faststream/middlewares/BaseMiddleware.md new file mode 100644 index 0000000000..aba3371bc8 --- /dev/null +++ b/docs/docs/en/api/faststream/middlewares/BaseMiddleware.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream._internal.middlewares.BaseMiddleware diff --git a/docs/docs/en/api/faststream/middlewares/ExceptionMiddleware.md b/docs/docs/en/api/faststream/middlewares/ExceptionMiddleware.md new file mode 100644 index 0000000000..c1d21850c8 --- /dev/null +++ b/docs/docs/en/api/faststream/middlewares/ExceptionMiddleware.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.middlewares.ExceptionMiddleware diff --git a/docs/docs/en/api/faststream/middlewares/acknowledgement/conf/AckPolicy.md b/docs/docs/en/api/faststream/middlewares/acknowledgement/conf/AckPolicy.md new file mode 100644 index 0000000000..8a92ec0a54 --- /dev/null +++ b/docs/docs/en/api/faststream/middlewares/acknowledgement/conf/AckPolicy.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.middlewares.acknowledgement.conf.AckPolicy diff --git a/docs/docs/en/api/faststream/middlewares/acknowledgement/middleware/AcknowledgementMiddleware.md b/docs/docs/en/api/faststream/middlewares/acknowledgement/middleware/AcknowledgementMiddleware.md new file mode 100644 index 0000000000..79b2956eb4 --- /dev/null +++ b/docs/docs/en/api/faststream/middlewares/acknowledgement/middleware/AcknowledgementMiddleware.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.middlewares.acknowledgement.middleware.AcknowledgementMiddleware diff --git a/docs/docs/en/api/faststream/middlewares/exception/ExceptionMiddleware.md b/docs/docs/en/api/faststream/middlewares/exception/ExceptionMiddleware.md new file mode 100644 index 0000000000..da1693a722 --- /dev/null +++ b/docs/docs/en/api/faststream/middlewares/exception/ExceptionMiddleware.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.middlewares.exception.ExceptionMiddleware diff --git a/docs/docs/en/api/faststream/middlewares/exception/ignore_handler.md b/docs/docs/en/api/faststream/middlewares/exception/ignore_handler.md new file mode 100644 index 0000000000..1eea49ddbe --- /dev/null +++ b/docs/docs/en/api/faststream/middlewares/exception/ignore_handler.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.middlewares.exception.ignore_handler diff --git a/docs/docs/en/api/faststream/middlewares/logging/CriticalLogMiddleware.md b/docs/docs/en/api/faststream/middlewares/logging/CriticalLogMiddleware.md new file mode 100644 index 0000000000..58be3830e6 --- /dev/null +++ b/docs/docs/en/api/faststream/middlewares/logging/CriticalLogMiddleware.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.middlewares.logging.CriticalLogMiddleware diff --git a/docs/docs/en/api/faststream/nats/PubAck.md b/docs/docs/en/api/faststream/nats/PubAck.md new file mode 100644 index 0000000000..697f22abe6 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/PubAck.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: nats.js.api.PubAck diff --git a/docs/docs/en/api/faststream/nats/TestApp.md b/docs/docs/en/api/faststream/nats/TestApp.md index 2468f3755c..ad101303af 100644 --- a/docs/docs/en/api/faststream/nats/TestApp.md +++ b/docs/docs/en/api/faststream/nats/TestApp.md @@ -8,4 +8,4 @@ search: boost: 0.5 --- -::: faststream.testing.app.TestApp +::: faststream._internal.testing.app.TestApp diff --git a/docs/docs/en/api/faststream/nats/broker/NatsPublisher.md b/docs/docs/en/api/faststream/nats/broker/NatsPublisher.md new file mode 100644 index 0000000000..3b54c47dff --- /dev/null +++ b/docs/docs/en/api/faststream/nats/broker/NatsPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.broker.NatsPublisher diff --git a/docs/docs/en/api/faststream/nats/broker/NatsRoute.md b/docs/docs/en/api/faststream/nats/broker/NatsRoute.md new file mode 100644 index 0000000000..a1f87a656e --- /dev/null +++ b/docs/docs/en/api/faststream/nats/broker/NatsRoute.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.broker.NatsRoute diff --git a/docs/docs/en/api/faststream/nats/broker/NatsRouter.md b/docs/docs/en/api/faststream/nats/broker/NatsRouter.md new file mode 100644 index 0000000000..dec9bcc774 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/broker/NatsRouter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.broker.NatsRouter diff --git a/docs/docs/en/api/faststream/nats/broker/logging/NatsLoggingBroker.md b/docs/docs/en/api/faststream/nats/broker/logging/NatsLoggingBroker.md deleted file mode 100644 index cd31396a61..0000000000 --- a/docs/docs/en/api/faststream/nats/broker/logging/NatsLoggingBroker.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.broker.logging.NatsLoggingBroker diff --git a/docs/docs/en/api/faststream/nats/broker/logging/NatsParamsStorage.md b/docs/docs/en/api/faststream/nats/broker/logging/NatsParamsStorage.md new file mode 100644 index 0000000000..25b77d4331 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/broker/logging/NatsParamsStorage.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.broker.logging.NatsParamsStorage diff --git a/docs/docs/en/api/faststream/nats/broker/router/NatsPublisher.md b/docs/docs/en/api/faststream/nats/broker/router/NatsPublisher.md new file mode 100644 index 0000000000..964d954040 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/broker/router/NatsPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.broker.router.NatsPublisher diff --git a/docs/docs/en/api/faststream/nats/broker/router/NatsRoute.md b/docs/docs/en/api/faststream/nats/broker/router/NatsRoute.md new file mode 100644 index 0000000000..6122d25fbb --- /dev/null +++ b/docs/docs/en/api/faststream/nats/broker/router/NatsRoute.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.broker.router.NatsRoute diff --git a/docs/docs/en/api/faststream/nats/broker/router/NatsRouter.md b/docs/docs/en/api/faststream/nats/broker/router/NatsRouter.md new file mode 100644 index 0000000000..b24d7ee047 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/broker/router/NatsRouter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.broker.router.NatsRouter diff --git a/docs/docs/en/api/faststream/nats/broker/state/BrokerState.md b/docs/docs/en/api/faststream/nats/broker/state/BrokerState.md new file mode 100644 index 0000000000..ed5dc00c35 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/broker/state/BrokerState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.broker.state.BrokerState diff --git a/docs/docs/en/api/faststream/nats/configs/NatsBrokerConfig.md b/docs/docs/en/api/faststream/nats/configs/NatsBrokerConfig.md new file mode 100644 index 0000000000..03cb0bbb04 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/configs/NatsBrokerConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.configs.NatsBrokerConfig diff --git a/docs/docs/en/api/faststream/nats/configs/broker/NatsBrokerConfig.md b/docs/docs/en/api/faststream/nats/configs/broker/NatsBrokerConfig.md new file mode 100644 index 0000000000..1edde134db --- /dev/null +++ b/docs/docs/en/api/faststream/nats/configs/broker/NatsBrokerConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.configs.broker.NatsBrokerConfig diff --git a/docs/docs/en/api/faststream/nats/fastapi/Context.md b/docs/docs/en/api/faststream/nats/fastapi/Context.md index f4240bb0da..99bf141f5c 100644 --- a/docs/docs/en/api/faststream/nats/fastapi/Context.md +++ b/docs/docs/en/api/faststream/nats/fastapi/Context.md @@ -8,4 +8,4 @@ search: boost: 0.5 --- -::: faststream.broker.fastapi.context.Context +::: faststream._internal.fastapi.context.Context diff --git a/docs/docs/en/api/faststream/nats/helpers/state/ConnectedState.md b/docs/docs/en/api/faststream/nats/helpers/state/ConnectedState.md new file mode 100644 index 0000000000..888302338b --- /dev/null +++ b/docs/docs/en/api/faststream/nats/helpers/state/ConnectedState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.helpers.state.ConnectedState diff --git a/docs/docs/en/api/faststream/nats/helpers/state/ConnectionState.md b/docs/docs/en/api/faststream/nats/helpers/state/ConnectionState.md new file mode 100644 index 0000000000..0d99fb56ed --- /dev/null +++ b/docs/docs/en/api/faststream/nats/helpers/state/ConnectionState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.helpers.state.ConnectionState diff --git a/docs/docs/en/api/faststream/nats/helpers/state/EmptyConnectionState.md b/docs/docs/en/api/faststream/nats/helpers/state/EmptyConnectionState.md new file mode 100644 index 0000000000..31a062d4ad --- /dev/null +++ b/docs/docs/en/api/faststream/nats/helpers/state/EmptyConnectionState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.helpers.state.EmptyConnectionState diff --git a/docs/docs/en/api/faststream/nats/publisher/asyncapi/AsyncAPIPublisher.md b/docs/docs/en/api/faststream/nats/publisher/asyncapi/AsyncAPIPublisher.md deleted file mode 100644 index 6ea394db59..0000000000 --- a/docs/docs/en/api/faststream/nats/publisher/asyncapi/AsyncAPIPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.publisher.asyncapi.AsyncAPIPublisher diff --git a/docs/docs/en/api/faststream/nats/publisher/config/NatsPublisherConfig.md b/docs/docs/en/api/faststream/nats/publisher/config/NatsPublisherConfig.md new file mode 100644 index 0000000000..4e335a7ea5 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/publisher/config/NatsPublisherConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.publisher.config.NatsPublisherConfig diff --git a/docs/docs/en/api/faststream/nats/publisher/config/NatsPublisherSpecificationConfig.md b/docs/docs/en/api/faststream/nats/publisher/config/NatsPublisherSpecificationConfig.md new file mode 100644 index 0000000000..3f21bb3e65 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/publisher/config/NatsPublisherSpecificationConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.publisher.config.NatsPublisherSpecificationConfig diff --git a/docs/docs/en/api/faststream/nats/publisher/factory/create_publisher.md b/docs/docs/en/api/faststream/nats/publisher/factory/create_publisher.md new file mode 100644 index 0000000000..19b23b99a5 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/publisher/factory/create_publisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.publisher.factory.create_publisher diff --git a/docs/docs/en/api/faststream/nats/publisher/fake/NatsFakePublisher.md b/docs/docs/en/api/faststream/nats/publisher/fake/NatsFakePublisher.md new file mode 100644 index 0000000000..df23cc8045 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/publisher/fake/NatsFakePublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.publisher.fake.NatsFakePublisher diff --git a/docs/docs/en/api/faststream/nats/publisher/producer/FakeNatsFastProducer.md b/docs/docs/en/api/faststream/nats/publisher/producer/FakeNatsFastProducer.md new file mode 100644 index 0000000000..57c4da65a4 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/publisher/producer/FakeNatsFastProducer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.publisher.producer.FakeNatsFastProducer diff --git a/docs/docs/en/api/faststream/nats/publisher/producer/NatsFastProducerImpl.md b/docs/docs/en/api/faststream/nats/publisher/producer/NatsFastProducerImpl.md new file mode 100644 index 0000000000..a6ec950230 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/publisher/producer/NatsFastProducerImpl.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.publisher.producer.NatsFastProducerImpl diff --git a/docs/docs/en/api/faststream/nats/publisher/specification/NatsPublisherSpecification.md b/docs/docs/en/api/faststream/nats/publisher/specification/NatsPublisherSpecification.md new file mode 100644 index 0000000000..d893bf2d0b --- /dev/null +++ b/docs/docs/en/api/faststream/nats/publisher/specification/NatsPublisherSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.publisher.specification.NatsPublisherSpecification diff --git a/docs/docs/en/api/faststream/nats/response/NatsPublishCommand.md b/docs/docs/en/api/faststream/nats/response/NatsPublishCommand.md new file mode 100644 index 0000000000..148119ba8a --- /dev/null +++ b/docs/docs/en/api/faststream/nats/response/NatsPublishCommand.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.response.NatsPublishCommand diff --git a/docs/docs/en/api/faststream/nats/router/NatsPublisher.md b/docs/docs/en/api/faststream/nats/router/NatsPublisher.md deleted file mode 100644 index b025495e44..0000000000 --- a/docs/docs/en/api/faststream/nats/router/NatsPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.router.NatsPublisher diff --git a/docs/docs/en/api/faststream/nats/router/NatsRoute.md b/docs/docs/en/api/faststream/nats/router/NatsRoute.md deleted file mode 100644 index 36df33c45e..0000000000 --- a/docs/docs/en/api/faststream/nats/router/NatsRoute.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.router.NatsRoute diff --git a/docs/docs/en/api/faststream/nats/router/NatsRouter.md b/docs/docs/en/api/faststream/nats/router/NatsRouter.md deleted file mode 100644 index 4b6dfaaf7d..0000000000 --- a/docs/docs/en/api/faststream/nats/router/NatsRouter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.router.NatsRouter diff --git a/docs/docs/en/api/faststream/nats/schemas/PubAck.md b/docs/docs/en/api/faststream/nats/schemas/PubAck.md new file mode 100644 index 0000000000..697f22abe6 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/schemas/PubAck.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: nats.js.api.PubAck diff --git a/docs/docs/en/api/faststream/nats/subscriber/adapters/UnsubscribeAdapter.md b/docs/docs/en/api/faststream/nats/subscriber/adapters/UnsubscribeAdapter.md new file mode 100644 index 0000000000..9b00a89428 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/adapters/UnsubscribeAdapter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.adapters.UnsubscribeAdapter diff --git a/docs/docs/en/api/faststream/nats/subscriber/adapters/Unsubscriptable.md b/docs/docs/en/api/faststream/nats/subscriber/adapters/Unsubscriptable.md new file mode 100644 index 0000000000..4c6c6b0abe --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/adapters/Unsubscriptable.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.adapters.Unsubscriptable diff --git a/docs/docs/en/api/faststream/nats/subscriber/adapters/Watchable.md b/docs/docs/en/api/faststream/nats/subscriber/adapters/Watchable.md new file mode 100644 index 0000000000..00dde78565 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/adapters/Watchable.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.adapters.Watchable diff --git a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIBatchPullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIBatchPullStreamSubscriber.md deleted file mode 100644 index 15bceeedbc..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIBatchPullStreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.asyncapi.AsyncAPIBatchPullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentCoreSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentCoreSubscriber.md deleted file mode 100644 index f88e14f817..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentCoreSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.asyncapi.AsyncAPIConcurrentCoreSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentPullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentPullStreamSubscriber.md deleted file mode 100644 index b5ebf86f93..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentPullStreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.asyncapi.AsyncAPIConcurrentPullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentPushStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentPushStreamSubscriber.md deleted file mode 100644 index 7bb4a6e088..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIConcurrentPushStreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.asyncapi.AsyncAPIConcurrentPushStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPICoreSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPICoreSubscriber.md deleted file mode 100644 index 8819adebab..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPICoreSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.asyncapi.AsyncAPICoreSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIKeyValueWatchSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIKeyValueWatchSubscriber.md deleted file mode 100644 index b006854b0b..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIKeyValueWatchSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.asyncapi.AsyncAPIKeyValueWatchSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIObjStoreWatchSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIObjStoreWatchSubscriber.md deleted file mode 100644 index 0a9157ed55..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIObjStoreWatchSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.asyncapi.AsyncAPIObjStoreWatchSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIPullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIPullStreamSubscriber.md deleted file mode 100644 index e9650bef94..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIPullStreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.asyncapi.AsyncAPIPullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIStreamSubscriber.md deleted file mode 100644 index 6d448d3af5..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPIStreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.asyncapi.AsyncAPIStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPISubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPISubscriber.md deleted file mode 100644 index 4fcbab6ea6..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/asyncapi/AsyncAPISubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.asyncapi.AsyncAPISubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/config/NatsSubscriberConfig.md b/docs/docs/en/api/faststream/nats/subscriber/config/NatsSubscriberConfig.md new file mode 100644 index 0000000000..7842c58ed6 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/config/NatsSubscriberConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.config.NatsSubscriberConfig diff --git a/docs/docs/en/api/faststream/nats/subscriber/config/NatsSubscriberSpecificationConfig.md b/docs/docs/en/api/faststream/nats/subscriber/config/NatsSubscriberSpecificationConfig.md new file mode 100644 index 0000000000..7c6514e8c0 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/config/NatsSubscriberSpecificationConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.config.NatsSubscriberSpecificationConfig diff --git a/docs/docs/en/api/faststream/nats/subscriber/specification/NatsSubscriberSpecification.md b/docs/docs/en/api/faststream/nats/subscriber/specification/NatsSubscriberSpecification.md new file mode 100644 index 0000000000..299cfc674f --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/specification/NatsSubscriberSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.specification.NatsSubscriberSpecification diff --git a/docs/docs/en/api/faststream/nats/subscriber/specification/NotIncludeSpecifation.md b/docs/docs/en/api/faststream/nats/subscriber/specification/NotIncludeSpecifation.md new file mode 100644 index 0000000000..2738a9ace5 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/specification/NotIncludeSpecifation.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.specification.NotIncludeSpecifation diff --git a/docs/docs/en/api/faststream/nats/subscriber/state/ConnectedSubscriberState.md b/docs/docs/en/api/faststream/nats/subscriber/state/ConnectedSubscriberState.md new file mode 100644 index 0000000000..3398403cb2 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/state/ConnectedSubscriberState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.state.ConnectedSubscriberState diff --git a/docs/docs/en/api/faststream/nats/subscriber/state/EmptySubscriberState.md b/docs/docs/en/api/faststream/nats/subscriber/state/EmptySubscriberState.md new file mode 100644 index 0000000000..de80057014 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/state/EmptySubscriberState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.state.EmptySubscriberState diff --git a/docs/docs/en/api/faststream/nats/subscriber/state/SubscriberState.md b/docs/docs/en/api/faststream/nats/subscriber/state/SubscriberState.md new file mode 100644 index 0000000000..a61839436a --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/state/SubscriberState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.state.SubscriberState diff --git a/docs/docs/en/api/faststream/nats/subscriber/subscription/UnsubscribeAdapter.md b/docs/docs/en/api/faststream/nats/subscriber/subscription/UnsubscribeAdapter.md deleted file mode 100644 index 455885671f..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/subscription/UnsubscribeAdapter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.subscription.UnsubscribeAdapter diff --git a/docs/docs/en/api/faststream/nats/subscriber/subscription/Unsubscriptable.md b/docs/docs/en/api/faststream/nats/subscriber/subscription/Unsubscriptable.md deleted file mode 100644 index c94cb1b731..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/subscription/Unsubscriptable.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.subscription.Unsubscriptable diff --git a/docs/docs/en/api/faststream/nats/subscriber/subscription/Watchable.md b/docs/docs/en/api/faststream/nats/subscriber/subscription/Watchable.md deleted file mode 100644 index 67638258ea..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/subscription/Watchable.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.subscription.Watchable diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecase/BatchPullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecase/BatchPullStreamSubscriber.md deleted file mode 100644 index dfb1c43575..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/usecase/BatchPullStreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.usecase.BatchPullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecase/ConcurrentCoreSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecase/ConcurrentCoreSubscriber.md deleted file mode 100644 index e1f100c043..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/usecase/ConcurrentCoreSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.usecase.ConcurrentCoreSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecase/ConcurrentPullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecase/ConcurrentPullStreamSubscriber.md deleted file mode 100644 index c1b7207285..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/usecase/ConcurrentPullStreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.usecase.ConcurrentPullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecase/ConcurrentPushStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecase/ConcurrentPushStreamSubscriber.md deleted file mode 100644 index ffa2e0c37b..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/usecase/ConcurrentPushStreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.usecase.ConcurrentPushStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecase/CoreSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecase/CoreSubscriber.md deleted file mode 100644 index 8ddb0b8c04..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/usecase/CoreSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.usecase.CoreSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecase/KeyValueWatchSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecase/KeyValueWatchSubscriber.md deleted file mode 100644 index 778557ee2b..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/usecase/KeyValueWatchSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.usecase.KeyValueWatchSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecase/LogicSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecase/LogicSubscriber.md deleted file mode 100644 index 100db07bbe..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/usecase/LogicSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.usecase.LogicSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecase/ObjStoreWatchSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecase/ObjStoreWatchSubscriber.md deleted file mode 100644 index ad15f32931..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/usecase/ObjStoreWatchSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.usecase.ObjStoreWatchSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecase/PullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecase/PullStreamSubscriber.md deleted file mode 100644 index 30f30a893f..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/usecase/PullStreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.usecase.PullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecase/PushStreamSubscription.md b/docs/docs/en/api/faststream/nats/subscriber/usecase/PushStreamSubscription.md deleted file mode 100644 index bb29bbb9c2..0000000000 --- a/docs/docs/en/api/faststream/nats/subscriber/usecase/PushStreamSubscription.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.nats.subscriber.usecase.PushStreamSubscription diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/BatchPullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/BatchPullStreamSubscriber.md new file mode 100644 index 0000000000..667ff42587 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/BatchPullStreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.BatchPullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/ConcurrentCoreSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/ConcurrentCoreSubscriber.md new file mode 100644 index 0000000000..bbd0895a8c --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/ConcurrentCoreSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.ConcurrentCoreSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/ConcurrentPullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/ConcurrentPullStreamSubscriber.md new file mode 100644 index 0000000000..7bb13db682 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/ConcurrentPullStreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.ConcurrentPullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/ConcurrentPushStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/ConcurrentPushStreamSubscriber.md new file mode 100644 index 0000000000..c694d41369 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/ConcurrentPushStreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.ConcurrentPushStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/CoreSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/CoreSubscriber.md new file mode 100644 index 0000000000..e35ebd3f9d --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/CoreSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.CoreSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/KeyValueWatchSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/KeyValueWatchSubscriber.md new file mode 100644 index 0000000000..507c7a33c4 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/KeyValueWatchSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.KeyValueWatchSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/LogicSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/LogicSubscriber.md new file mode 100644 index 0000000000..e97348563d --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/LogicSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.LogicSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/ObjStoreWatchSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/ObjStoreWatchSubscriber.md new file mode 100644 index 0000000000..25d1434968 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/ObjStoreWatchSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.ObjStoreWatchSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/PullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/PullStreamSubscriber.md new file mode 100644 index 0000000000..ddc3731c6e --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/PullStreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.PullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/PushStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/PushStreamSubscriber.md new file mode 100644 index 0000000000..432615d05b --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/PushStreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.PushStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/basic/DefaultSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/basic/DefaultSubscriber.md new file mode 100644 index 0000000000..f0bd5e52aa --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/basic/DefaultSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.basic.DefaultSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/basic/LogicSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/basic/LogicSubscriber.md new file mode 100644 index 0000000000..9dfc93aa9b --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/basic/LogicSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.basic.LogicSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/core_subscriber/ConcurrentCoreSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/core_subscriber/ConcurrentCoreSubscriber.md new file mode 100644 index 0000000000..a684452aa1 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/core_subscriber/ConcurrentCoreSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.core_subscriber.ConcurrentCoreSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/core_subscriber/CoreSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/core_subscriber/CoreSubscriber.md new file mode 100644 index 0000000000..cc1905f58e --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/core_subscriber/CoreSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.core_subscriber.CoreSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/key_value_subscriber/KeyValueWatchSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/key_value_subscriber/KeyValueWatchSubscriber.md new file mode 100644 index 0000000000..26e1b670d0 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/key_value_subscriber/KeyValueWatchSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.key_value_subscriber.KeyValueWatchSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/object_storage_subscriber/ObjStoreWatchSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/object_storage_subscriber/ObjStoreWatchSubscriber.md new file mode 100644 index 0000000000..b1722e57ec --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/object_storage_subscriber/ObjStoreWatchSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.object_storage_subscriber.ObjStoreWatchSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_basic/StreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_basic/StreamSubscriber.md new file mode 100644 index 0000000000..b08fd2abff --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_basic/StreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.stream_basic.StreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_pull_subscriber/BatchPullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_pull_subscriber/BatchPullStreamSubscriber.md new file mode 100644 index 0000000000..9e4973cf67 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_pull_subscriber/BatchPullStreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.stream_pull_subscriber.BatchPullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_pull_subscriber/ConcurrentPullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_pull_subscriber/ConcurrentPullStreamSubscriber.md new file mode 100644 index 0000000000..6c31f93a20 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_pull_subscriber/ConcurrentPullStreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.stream_pull_subscriber.ConcurrentPullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_pull_subscriber/PullStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_pull_subscriber/PullStreamSubscriber.md new file mode 100644 index 0000000000..35e7de26de --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_pull_subscriber/PullStreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.stream_pull_subscriber.PullStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_push_subscriber/ConcurrentPushStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_push_subscriber/ConcurrentPushStreamSubscriber.md new file mode 100644 index 0000000000..78cffa0a9f --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_push_subscriber/ConcurrentPushStreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.stream_push_subscriber.ConcurrentPushStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_push_subscriber/PushStreamSubscriber.md b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_push_subscriber/PushStreamSubscriber.md new file mode 100644 index 0000000000..9151b90b61 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/subscriber/usecases/stream_push_subscriber/PushStreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.subscriber.usecases.stream_push_subscriber.PushStreamSubscriber diff --git a/docs/docs/en/api/faststream/nats/testing/change_producer.md b/docs/docs/en/api/faststream/nats/testing/change_producer.md new file mode 100644 index 0000000000..fba3211347 --- /dev/null +++ b/docs/docs/en/api/faststream/nats/testing/change_producer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.nats.testing.change_producer diff --git a/docs/docs/en/api/faststream/params/Context.md b/docs/docs/en/api/faststream/params/Context.md new file mode 100644 index 0000000000..8eb7fc249a --- /dev/null +++ b/docs/docs/en/api/faststream/params/Context.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.params.Context diff --git a/docs/docs/en/api/faststream/utils/Depends.md b/docs/docs/en/api/faststream/params/Depends.md similarity index 100% rename from docs/docs/en/api/faststream/utils/Depends.md rename to docs/docs/en/api/faststream/params/Depends.md diff --git a/docs/docs/en/api/faststream/params/Header.md b/docs/docs/en/api/faststream/params/Header.md new file mode 100644 index 0000000000..f3dd71365c --- /dev/null +++ b/docs/docs/en/api/faststream/params/Header.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.params.Header diff --git a/docs/docs/en/api/faststream/params/Path.md b/docs/docs/en/api/faststream/params/Path.md new file mode 100644 index 0000000000..ad04fdff6e --- /dev/null +++ b/docs/docs/en/api/faststream/params/Path.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.params.Path diff --git a/docs/docs/en/api/faststream/params/no_cast/NoCastField.md b/docs/docs/en/api/faststream/params/no_cast/NoCastField.md new file mode 100644 index 0000000000..56821cae8a --- /dev/null +++ b/docs/docs/en/api/faststream/params/no_cast/NoCastField.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.params.no_cast.NoCastField diff --git a/docs/docs/en/api/faststream/params/params/Context.md b/docs/docs/en/api/faststream/params/params/Context.md new file mode 100644 index 0000000000..4c3ec6b4dd --- /dev/null +++ b/docs/docs/en/api/faststream/params/params/Context.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.params.params.Context diff --git a/docs/docs/en/api/faststream/params/params/Header.md b/docs/docs/en/api/faststream/params/params/Header.md new file mode 100644 index 0000000000..6b15bd1ec1 --- /dev/null +++ b/docs/docs/en/api/faststream/params/params/Header.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.params.params.Header diff --git a/docs/docs/en/api/faststream/params/params/Path.md b/docs/docs/en/api/faststream/params/params/Path.md new file mode 100644 index 0000000000..0903f40023 --- /dev/null +++ b/docs/docs/en/api/faststream/params/params/Path.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.params.params.Path diff --git a/docs/docs/en/api/faststream/prometheus/BasePrometheusMiddleware.md b/docs/docs/en/api/faststream/prometheus/BasePrometheusMiddleware.md deleted file mode 100644 index 1f5cf6a1d4..0000000000 --- a/docs/docs/en/api/faststream/prometheus/BasePrometheusMiddleware.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.prometheus.BasePrometheusMiddleware diff --git a/docs/docs/en/api/faststream/prometheus/PrometheusMiddleware.md b/docs/docs/en/api/faststream/prometheus/PrometheusMiddleware.md new file mode 100644 index 0000000000..c340a0cb23 --- /dev/null +++ b/docs/docs/en/api/faststream/prometheus/PrometheusMiddleware.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.prometheus.PrometheusMiddleware diff --git a/docs/docs/en/api/faststream/rabbit/ReplyConfig.md b/docs/docs/en/api/faststream/rabbit/ReplyConfig.md deleted file mode 100644 index 013bd2f986..0000000000 --- a/docs/docs/en/api/faststream/rabbit/ReplyConfig.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.ReplyConfig diff --git a/docs/docs/en/api/faststream/rabbit/TestApp.md b/docs/docs/en/api/faststream/rabbit/TestApp.md index 2468f3755c..ad101303af 100644 --- a/docs/docs/en/api/faststream/rabbit/TestApp.md +++ b/docs/docs/en/api/faststream/rabbit/TestApp.md @@ -8,4 +8,4 @@ search: boost: 0.5 --- -::: faststream.testing.app.TestApp +::: faststream._internal.testing.app.TestApp diff --git a/docs/docs/en/api/faststream/rabbit/broker/RabbitPublisher.md b/docs/docs/en/api/faststream/rabbit/broker/RabbitPublisher.md new file mode 100644 index 0000000000..6790682e9a --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/broker/RabbitPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.broker.RabbitPublisher diff --git a/docs/docs/en/api/faststream/rabbit/broker/RabbitRoute.md b/docs/docs/en/api/faststream/rabbit/broker/RabbitRoute.md new file mode 100644 index 0000000000..0cb754f304 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/broker/RabbitRoute.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.broker.RabbitRoute diff --git a/docs/docs/en/api/faststream/rabbit/broker/RabbitRouter.md b/docs/docs/en/api/faststream/rabbit/broker/RabbitRouter.md new file mode 100644 index 0000000000..69aad18bb3 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/broker/RabbitRouter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.broker.RabbitRouter diff --git a/docs/docs/en/api/faststream/rabbit/broker/logging/RabbitLoggingBroker.md b/docs/docs/en/api/faststream/rabbit/broker/logging/RabbitLoggingBroker.md deleted file mode 100644 index a3b3151d4b..0000000000 --- a/docs/docs/en/api/faststream/rabbit/broker/logging/RabbitLoggingBroker.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.broker.logging.RabbitLoggingBroker diff --git a/docs/docs/en/api/faststream/rabbit/broker/logging/RabbitParamsStorage.md b/docs/docs/en/api/faststream/rabbit/broker/logging/RabbitParamsStorage.md new file mode 100644 index 0000000000..e9e46da6af --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/broker/logging/RabbitParamsStorage.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.broker.logging.RabbitParamsStorage diff --git a/docs/docs/en/api/faststream/rabbit/broker/router/RabbitPublisher.md b/docs/docs/en/api/faststream/rabbit/broker/router/RabbitPublisher.md new file mode 100644 index 0000000000..e8dc979160 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/broker/router/RabbitPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.broker.router.RabbitPublisher diff --git a/docs/docs/en/api/faststream/rabbit/broker/router/RabbitRoute.md b/docs/docs/en/api/faststream/rabbit/broker/router/RabbitRoute.md new file mode 100644 index 0000000000..eb8e9a8edb --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/broker/router/RabbitRoute.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.broker.router.RabbitRoute diff --git a/docs/docs/en/api/faststream/rabbit/broker/router/RabbitRouter.md b/docs/docs/en/api/faststream/rabbit/broker/router/RabbitRouter.md new file mode 100644 index 0000000000..8ac8758c9e --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/broker/router/RabbitRouter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.broker.router.RabbitRouter diff --git a/docs/docs/en/api/faststream/rabbit/configs/RabbitBrokerConfig.md b/docs/docs/en/api/faststream/rabbit/configs/RabbitBrokerConfig.md new file mode 100644 index 0000000000..6f0bd0be95 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/configs/RabbitBrokerConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.configs.RabbitBrokerConfig diff --git a/docs/docs/en/api/faststream/rabbit/configs/base/RabbitConfig.md b/docs/docs/en/api/faststream/rabbit/configs/base/RabbitConfig.md new file mode 100644 index 0000000000..88c2372085 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/configs/base/RabbitConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.configs.base.RabbitConfig diff --git a/docs/docs/en/api/faststream/rabbit/configs/base/RabbitEndpointConfig.md b/docs/docs/en/api/faststream/rabbit/configs/base/RabbitEndpointConfig.md new file mode 100644 index 0000000000..ad5f2bd204 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/configs/base/RabbitEndpointConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.configs.base.RabbitEndpointConfig diff --git a/docs/docs/en/api/faststream/rabbit/configs/broker/RabbitBrokerConfig.md b/docs/docs/en/api/faststream/rabbit/configs/broker/RabbitBrokerConfig.md new file mode 100644 index 0000000000..babd1f6a62 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/configs/broker/RabbitBrokerConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.configs.broker.RabbitBrokerConfig diff --git a/docs/docs/en/api/faststream/rabbit/fastapi/Context.md b/docs/docs/en/api/faststream/rabbit/fastapi/Context.md index f4240bb0da..99bf141f5c 100644 --- a/docs/docs/en/api/faststream/rabbit/fastapi/Context.md +++ b/docs/docs/en/api/faststream/rabbit/fastapi/Context.md @@ -8,4 +8,4 @@ search: boost: 0.5 --- -::: faststream.broker.fastapi.context.Context +::: faststream._internal.fastapi.context.Context diff --git a/docs/docs/en/api/faststream/rabbit/fastapi/fastapi/RabbitRouter.md b/docs/docs/en/api/faststream/rabbit/fastapi/fastapi/RabbitRouter.md new file mode 100644 index 0000000000..d70c558254 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/fastapi/fastapi/RabbitRouter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.fastapi.fastapi.RabbitRouter diff --git a/docs/docs/en/api/faststream/rabbit/fastapi/router/RabbitRouter.md b/docs/docs/en/api/faststream/rabbit/fastapi/router/RabbitRouter.md deleted file mode 100644 index 36dda03314..0000000000 --- a/docs/docs/en/api/faststream/rabbit/fastapi/router/RabbitRouter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.fastapi.router.RabbitRouter diff --git a/docs/docs/en/api/faststream/rabbit/helpers/channel_manager/ChannelManagerImpl.md b/docs/docs/en/api/faststream/rabbit/helpers/channel_manager/ChannelManagerImpl.md new file mode 100644 index 0000000000..b020821ec8 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/helpers/channel_manager/ChannelManagerImpl.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.helpers.channel_manager.ChannelManagerImpl diff --git a/docs/docs/en/api/faststream/rabbit/helpers/channel_manager/FakeChannelManager.md b/docs/docs/en/api/faststream/rabbit/helpers/channel_manager/FakeChannelManager.md new file mode 100644 index 0000000000..2ec3966bde --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/helpers/channel_manager/FakeChannelManager.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.helpers.channel_manager.FakeChannelManager diff --git a/docs/docs/en/api/faststream/rabbit/helpers/declarer/FakeRabbitDeclarer.md b/docs/docs/en/api/faststream/rabbit/helpers/declarer/FakeRabbitDeclarer.md new file mode 100644 index 0000000000..f348fb0e98 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/helpers/declarer/FakeRabbitDeclarer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.helpers.declarer.FakeRabbitDeclarer diff --git a/docs/docs/en/api/faststream/rabbit/helpers/declarer/RabbitDeclarerImpl.md b/docs/docs/en/api/faststream/rabbit/helpers/declarer/RabbitDeclarerImpl.md new file mode 100644 index 0000000000..b19bc0ba21 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/helpers/declarer/RabbitDeclarerImpl.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.helpers.declarer.RabbitDeclarerImpl diff --git a/docs/docs/en/api/faststream/rabbit/helpers/state/ConnectedState.md b/docs/docs/en/api/faststream/rabbit/helpers/state/ConnectedState.md new file mode 100644 index 0000000000..db97303aa3 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/helpers/state/ConnectedState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.helpers.state.ConnectedState diff --git a/docs/docs/en/api/faststream/rabbit/helpers/state/ConnectionState.md b/docs/docs/en/api/faststream/rabbit/helpers/state/ConnectionState.md new file mode 100644 index 0000000000..36b3d4d4d1 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/helpers/state/ConnectionState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.helpers.state.ConnectionState diff --git a/docs/docs/en/api/faststream/rabbit/helpers/state/EmptyConnectionState.md b/docs/docs/en/api/faststream/rabbit/helpers/state/EmptyConnectionState.md new file mode 100644 index 0000000000..7b0af42897 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/helpers/state/EmptyConnectionState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.helpers.state.EmptyConnectionState diff --git a/docs/docs/en/api/faststream/rabbit/publisher/RabbitPublisher.md b/docs/docs/en/api/faststream/rabbit/publisher/RabbitPublisher.md new file mode 100644 index 0000000000..0ba15ce3d3 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/RabbitPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.RabbitPublisher diff --git a/docs/docs/en/api/faststream/rabbit/publisher/asyncapi/AsyncAPIPublisher.md b/docs/docs/en/api/faststream/rabbit/publisher/asyncapi/AsyncAPIPublisher.md deleted file mode 100644 index 6ece65cfed..0000000000 --- a/docs/docs/en/api/faststream/rabbit/publisher/asyncapi/AsyncAPIPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.publisher.asyncapi.AsyncAPIPublisher diff --git a/docs/docs/en/api/faststream/rabbit/publisher/config/RabbitPublisherConfig.md b/docs/docs/en/api/faststream/rabbit/publisher/config/RabbitPublisherConfig.md new file mode 100644 index 0000000000..498897de32 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/config/RabbitPublisherConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.config.RabbitPublisherConfig diff --git a/docs/docs/en/api/faststream/rabbit/publisher/config/RabbitPublisherSpecificationConfig.md b/docs/docs/en/api/faststream/rabbit/publisher/config/RabbitPublisherSpecificationConfig.md new file mode 100644 index 0000000000..71da5a1448 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/config/RabbitPublisherSpecificationConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.config.RabbitPublisherSpecificationConfig diff --git a/docs/docs/en/api/faststream/rabbit/publisher/factory/create_publisher.md b/docs/docs/en/api/faststream/rabbit/publisher/factory/create_publisher.md new file mode 100644 index 0000000000..bac090fa43 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/factory/create_publisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.factory.create_publisher diff --git a/docs/docs/en/api/faststream/rabbit/publisher/fake/RabbitFakePublisher.md b/docs/docs/en/api/faststream/rabbit/publisher/fake/RabbitFakePublisher.md new file mode 100644 index 0000000000..60879c8e3a --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/fake/RabbitFakePublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.fake.RabbitFakePublisher diff --git a/docs/docs/en/api/faststream/rabbit/publisher/options/MessageOptions.md b/docs/docs/en/api/faststream/rabbit/publisher/options/MessageOptions.md new file mode 100644 index 0000000000..eaa454588a --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/options/MessageOptions.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.options.MessageOptions diff --git a/docs/docs/en/api/faststream/rabbit/publisher/options/PublishKwargs.md b/docs/docs/en/api/faststream/rabbit/publisher/options/PublishKwargs.md new file mode 100644 index 0000000000..f1a127fad5 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/options/PublishKwargs.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.options.PublishKwargs diff --git a/docs/docs/en/api/faststream/rabbit/publisher/options/PublishOptions.md b/docs/docs/en/api/faststream/rabbit/publisher/options/PublishOptions.md new file mode 100644 index 0000000000..c80cc9e937 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/options/PublishOptions.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.options.PublishOptions diff --git a/docs/docs/en/api/faststream/rabbit/publisher/options/RequestPublishKwargs.md b/docs/docs/en/api/faststream/rabbit/publisher/options/RequestPublishKwargs.md new file mode 100644 index 0000000000..73fae44b84 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/options/RequestPublishKwargs.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.options.RequestPublishKwargs diff --git a/docs/docs/en/api/faststream/rabbit/publisher/producer/AioPikaFastProducerImpl.md b/docs/docs/en/api/faststream/rabbit/publisher/producer/AioPikaFastProducerImpl.md new file mode 100644 index 0000000000..dbe7f76435 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/producer/AioPikaFastProducerImpl.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.producer.AioPikaFastProducerImpl diff --git a/docs/docs/en/api/faststream/rabbit/publisher/producer/FakeAioPikaFastProducer.md b/docs/docs/en/api/faststream/rabbit/publisher/producer/FakeAioPikaFastProducer.md new file mode 100644 index 0000000000..678e2b596d --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/producer/FakeAioPikaFastProducer.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.producer.FakeAioPikaFastProducer diff --git a/docs/docs/en/api/faststream/rabbit/publisher/producer/LockState.md b/docs/docs/en/api/faststream/rabbit/publisher/producer/LockState.md new file mode 100644 index 0000000000..4d7b37ba46 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/producer/LockState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.producer.LockState diff --git a/docs/docs/en/api/faststream/rabbit/publisher/producer/LockUnset.md b/docs/docs/en/api/faststream/rabbit/publisher/producer/LockUnset.md new file mode 100644 index 0000000000..95df1a10e7 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/producer/LockUnset.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.producer.LockUnset diff --git a/docs/docs/en/api/faststream/rabbit/publisher/producer/RealLock.md b/docs/docs/en/api/faststream/rabbit/publisher/producer/RealLock.md new file mode 100644 index 0000000000..570a279a0a --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/producer/RealLock.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.producer.RealLock diff --git a/docs/docs/en/api/faststream/rabbit/publisher/specification/RabbitPublisherSpecification.md b/docs/docs/en/api/faststream/rabbit/publisher/specification/RabbitPublisherSpecification.md new file mode 100644 index 0000000000..efef9e5e3a --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/specification/RabbitPublisherSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.specification.RabbitPublisherSpecification diff --git a/docs/docs/en/api/faststream/rabbit/publisher/usecase/LogicPublisher.md b/docs/docs/en/api/faststream/rabbit/publisher/usecase/LogicPublisher.md deleted file mode 100644 index 1ef927866e..0000000000 --- a/docs/docs/en/api/faststream/rabbit/publisher/usecase/LogicPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.publisher.usecase.LogicPublisher diff --git a/docs/docs/en/api/faststream/rabbit/publisher/usecase/PublishKwargs.md b/docs/docs/en/api/faststream/rabbit/publisher/usecase/PublishKwargs.md deleted file mode 100644 index 3d917891cd..0000000000 --- a/docs/docs/en/api/faststream/rabbit/publisher/usecase/PublishKwargs.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.publisher.usecase.PublishKwargs diff --git a/docs/docs/en/api/faststream/rabbit/publisher/usecase/RabbitPublisher.md b/docs/docs/en/api/faststream/rabbit/publisher/usecase/RabbitPublisher.md new file mode 100644 index 0000000000..9e2d36c4dd --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/publisher/usecase/RabbitPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.publisher.usecase.RabbitPublisher diff --git a/docs/docs/en/api/faststream/rabbit/publisher/usecase/RequestPublishKwargs.md b/docs/docs/en/api/faststream/rabbit/publisher/usecase/RequestPublishKwargs.md deleted file mode 100644 index 5668633016..0000000000 --- a/docs/docs/en/api/faststream/rabbit/publisher/usecase/RequestPublishKwargs.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.publisher.usecase.RequestPublishKwargs diff --git a/docs/docs/en/api/faststream/rabbit/response/RabbitPublishCommand.md b/docs/docs/en/api/faststream/rabbit/response/RabbitPublishCommand.md new file mode 100644 index 0000000000..4c4bb224b6 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/response/RabbitPublishCommand.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.response.RabbitPublishCommand diff --git a/docs/docs/en/api/faststream/rabbit/router/RabbitPublisher.md b/docs/docs/en/api/faststream/rabbit/router/RabbitPublisher.md deleted file mode 100644 index befbec9103..0000000000 --- a/docs/docs/en/api/faststream/rabbit/router/RabbitPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.router.RabbitPublisher diff --git a/docs/docs/en/api/faststream/rabbit/router/RabbitRoute.md b/docs/docs/en/api/faststream/rabbit/router/RabbitRoute.md deleted file mode 100644 index 8e8b0fbb6c..0000000000 --- a/docs/docs/en/api/faststream/rabbit/router/RabbitRoute.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.router.RabbitRoute diff --git a/docs/docs/en/api/faststream/rabbit/router/RabbitRouter.md b/docs/docs/en/api/faststream/rabbit/router/RabbitRouter.md deleted file mode 100644 index eff5f6169a..0000000000 --- a/docs/docs/en/api/faststream/rabbit/router/RabbitRouter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.router.RabbitRouter diff --git a/docs/docs/en/api/faststream/rabbit/schemas/ReplyConfig.md b/docs/docs/en/api/faststream/rabbit/schemas/ReplyConfig.md deleted file mode 100644 index 239c4f9d6e..0000000000 --- a/docs/docs/en/api/faststream/rabbit/schemas/ReplyConfig.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.schemas.ReplyConfig diff --git a/docs/docs/en/api/faststream/rabbit/schemas/reply/ReplyConfig.md b/docs/docs/en/api/faststream/rabbit/schemas/reply/ReplyConfig.md deleted file mode 100644 index 1aeb941ff5..0000000000 --- a/docs/docs/en/api/faststream/rabbit/schemas/reply/ReplyConfig.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.schemas.reply.ReplyConfig diff --git a/docs/docs/en/api/faststream/rabbit/subscriber/RabbitSubscriber.md b/docs/docs/en/api/faststream/rabbit/subscriber/RabbitSubscriber.md new file mode 100644 index 0000000000..de7e855172 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/subscriber/RabbitSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.subscriber.RabbitSubscriber diff --git a/docs/docs/en/api/faststream/rabbit/subscriber/asyncapi/AsyncAPISubscriber.md b/docs/docs/en/api/faststream/rabbit/subscriber/asyncapi/AsyncAPISubscriber.md deleted file mode 100644 index 4d11c4b8e0..0000000000 --- a/docs/docs/en/api/faststream/rabbit/subscriber/asyncapi/AsyncAPISubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.subscriber.asyncapi.AsyncAPISubscriber diff --git a/docs/docs/en/api/faststream/rabbit/subscriber/config/RabbitSubscriberConfig.md b/docs/docs/en/api/faststream/rabbit/subscriber/config/RabbitSubscriberConfig.md new file mode 100644 index 0000000000..ec8ea45fd4 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/subscriber/config/RabbitSubscriberConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.subscriber.config.RabbitSubscriberConfig diff --git a/docs/docs/en/api/faststream/rabbit/subscriber/config/RabbitSubscriberSpecificationConfig.md b/docs/docs/en/api/faststream/rabbit/subscriber/config/RabbitSubscriberSpecificationConfig.md new file mode 100644 index 0000000000..48c64a10cc --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/subscriber/config/RabbitSubscriberSpecificationConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.subscriber.config.RabbitSubscriberSpecificationConfig diff --git a/docs/docs/en/api/faststream/rabbit/subscriber/specification/RabbitSubscriberSpecification.md b/docs/docs/en/api/faststream/rabbit/subscriber/specification/RabbitSubscriberSpecification.md new file mode 100644 index 0000000000..8bd1f66568 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/subscriber/specification/RabbitSubscriberSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.subscriber.specification.RabbitSubscriberSpecification diff --git a/docs/docs/en/api/faststream/rabbit/subscriber/usecase/LogicSubscriber.md b/docs/docs/en/api/faststream/rabbit/subscriber/usecase/LogicSubscriber.md deleted file mode 100644 index 56ef70dd0d..0000000000 --- a/docs/docs/en/api/faststream/rabbit/subscriber/usecase/LogicSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.rabbit.subscriber.usecase.LogicSubscriber diff --git a/docs/docs/en/api/faststream/rabbit/subscriber/usecase/RabbitSubscriber.md b/docs/docs/en/api/faststream/rabbit/subscriber/usecase/RabbitSubscriber.md new file mode 100644 index 0000000000..4678b38935 --- /dev/null +++ b/docs/docs/en/api/faststream/rabbit/subscriber/usecase/RabbitSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.rabbit.subscriber.usecase.RabbitSubscriber diff --git a/docs/docs/en/api/faststream/redis/TestApp.md b/docs/docs/en/api/faststream/redis/TestApp.md index 2468f3755c..ad101303af 100644 --- a/docs/docs/en/api/faststream/redis/TestApp.md +++ b/docs/docs/en/api/faststream/redis/TestApp.md @@ -8,4 +8,4 @@ search: boost: 0.5 --- -::: faststream.testing.app.TestApp +::: faststream._internal.testing.app.TestApp diff --git a/docs/docs/en/api/faststream/redis/broker/RedisBroker.md b/docs/docs/en/api/faststream/redis/broker/RedisBroker.md new file mode 100644 index 0000000000..ad22e9e965 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/broker/RedisBroker.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.broker.RedisBroker diff --git a/docs/docs/en/api/faststream/redis/broker/RedisPublisher.md b/docs/docs/en/api/faststream/redis/broker/RedisPublisher.md new file mode 100644 index 0000000000..9c9a440475 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/broker/RedisPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.broker.RedisPublisher diff --git a/docs/docs/en/api/faststream/redis/broker/RedisRoute.md b/docs/docs/en/api/faststream/redis/broker/RedisRoute.md new file mode 100644 index 0000000000..4423056a04 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/broker/RedisRoute.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.broker.RedisRoute diff --git a/docs/docs/en/api/faststream/redis/broker/RedisRouter.md b/docs/docs/en/api/faststream/redis/broker/RedisRouter.md new file mode 100644 index 0000000000..6df9e2ee8b --- /dev/null +++ b/docs/docs/en/api/faststream/redis/broker/RedisRouter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.broker.RedisRouter diff --git a/docs/docs/en/api/faststream/redis/broker/logging/RedisLoggingBroker.md b/docs/docs/en/api/faststream/redis/broker/logging/RedisLoggingBroker.md deleted file mode 100644 index 58500b3c1f..0000000000 --- a/docs/docs/en/api/faststream/redis/broker/logging/RedisLoggingBroker.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.broker.logging.RedisLoggingBroker diff --git a/docs/docs/en/api/faststream/redis/broker/logging/RedisParamsStorage.md b/docs/docs/en/api/faststream/redis/broker/logging/RedisParamsStorage.md new file mode 100644 index 0000000000..b7d1bb680a --- /dev/null +++ b/docs/docs/en/api/faststream/redis/broker/logging/RedisParamsStorage.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.broker.logging.RedisParamsStorage diff --git a/docs/docs/en/api/faststream/redis/broker/router/RedisPublisher.md b/docs/docs/en/api/faststream/redis/broker/router/RedisPublisher.md new file mode 100644 index 0000000000..701ea77a65 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/broker/router/RedisPublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.broker.router.RedisPublisher diff --git a/docs/docs/en/api/faststream/redis/broker/router/RedisRoute.md b/docs/docs/en/api/faststream/redis/broker/router/RedisRoute.md new file mode 100644 index 0000000000..e99e85df4d --- /dev/null +++ b/docs/docs/en/api/faststream/redis/broker/router/RedisRoute.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.broker.router.RedisRoute diff --git a/docs/docs/en/api/faststream/redis/broker/router/RedisRouter.md b/docs/docs/en/api/faststream/redis/broker/router/RedisRouter.md new file mode 100644 index 0000000000..ae4c03cf89 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/broker/router/RedisRouter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.broker.router.RedisRouter diff --git a/docs/docs/en/api/faststream/redis/configs/ConnectionState.md b/docs/docs/en/api/faststream/redis/configs/ConnectionState.md new file mode 100644 index 0000000000..b5b7100fee --- /dev/null +++ b/docs/docs/en/api/faststream/redis/configs/ConnectionState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.configs.ConnectionState diff --git a/docs/docs/en/api/faststream/redis/configs/RedisBrokerConfig.md b/docs/docs/en/api/faststream/redis/configs/RedisBrokerConfig.md new file mode 100644 index 0000000000..9d34faba85 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/configs/RedisBrokerConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.configs.RedisBrokerConfig diff --git a/docs/docs/en/api/faststream/redis/configs/broker/RedisBrokerConfig.md b/docs/docs/en/api/faststream/redis/configs/broker/RedisBrokerConfig.md new file mode 100644 index 0000000000..bca3e43d93 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/configs/broker/RedisBrokerConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.configs.broker.RedisBrokerConfig diff --git a/docs/docs/en/api/faststream/redis/configs/broker/RedisRouterConfig.md b/docs/docs/en/api/faststream/redis/configs/broker/RedisRouterConfig.md new file mode 100644 index 0000000000..3ef64ee0d5 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/configs/broker/RedisRouterConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.configs.broker.RedisRouterConfig diff --git a/docs/docs/en/api/faststream/redis/configs/state/ConnectionState.md b/docs/docs/en/api/faststream/redis/configs/state/ConnectionState.md new file mode 100644 index 0000000000..d9072a692a --- /dev/null +++ b/docs/docs/en/api/faststream/redis/configs/state/ConnectionState.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.configs.state.ConnectionState diff --git a/docs/docs/en/api/faststream/redis/fastapi/Context.md b/docs/docs/en/api/faststream/redis/fastapi/Context.md index f4240bb0da..99bf141f5c 100644 --- a/docs/docs/en/api/faststream/redis/fastapi/Context.md +++ b/docs/docs/en/api/faststream/redis/fastapi/Context.md @@ -8,4 +8,4 @@ search: boost: 0.5 --- -::: faststream.broker.fastapi.context.Context +::: faststream._internal.fastapi.context.Context diff --git a/docs/docs/en/api/faststream/redis/message/ListMessage.md b/docs/docs/en/api/faststream/redis/message/ListMessage.md deleted file mode 100644 index 5e81a9f727..0000000000 --- a/docs/docs/en/api/faststream/redis/message/ListMessage.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.message.ListMessage diff --git a/docs/docs/en/api/faststream/redis/message/StreamMessage.md b/docs/docs/en/api/faststream/redis/message/StreamMessage.md deleted file mode 100644 index f4e6a5d57e..0000000000 --- a/docs/docs/en/api/faststream/redis/message/StreamMessage.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.message.StreamMessage diff --git a/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIChannelPublisher.md b/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIChannelPublisher.md deleted file mode 100644 index a3bef9a56c..0000000000 --- a/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIChannelPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.publisher.asyncapi.AsyncAPIChannelPublisher diff --git a/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIListBatchPublisher.md b/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIListBatchPublisher.md deleted file mode 100644 index ab4361bd85..0000000000 --- a/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIListBatchPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.publisher.asyncapi.AsyncAPIListBatchPublisher diff --git a/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIListPublisher.md b/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIListPublisher.md deleted file mode 100644 index 0c233cc74b..0000000000 --- a/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIListPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.publisher.asyncapi.AsyncAPIListPublisher diff --git a/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIPublisher.md b/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIPublisher.md deleted file mode 100644 index 4243308fb7..0000000000 --- a/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.publisher.asyncapi.AsyncAPIPublisher diff --git a/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIStreamPublisher.md b/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIStreamPublisher.md deleted file mode 100644 index 29fb6329f3..0000000000 --- a/docs/docs/en/api/faststream/redis/publisher/asyncapi/AsyncAPIStreamPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.publisher.asyncapi.AsyncAPIStreamPublisher diff --git a/docs/docs/en/api/faststream/redis/publisher/config/RedisPublisherConfig.md b/docs/docs/en/api/faststream/redis/publisher/config/RedisPublisherConfig.md new file mode 100644 index 0000000000..016a941d24 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/publisher/config/RedisPublisherConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.publisher.config.RedisPublisherConfig diff --git a/docs/docs/en/api/faststream/redis/publisher/config/RedisPublisherSpecificationConfig.md b/docs/docs/en/api/faststream/redis/publisher/config/RedisPublisherSpecificationConfig.md new file mode 100644 index 0000000000..d60dcde407 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/publisher/config/RedisPublisherSpecificationConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.publisher.config.RedisPublisherSpecificationConfig diff --git a/docs/docs/en/api/faststream/redis/publisher/factory/create_publisher.md b/docs/docs/en/api/faststream/redis/publisher/factory/create_publisher.md new file mode 100644 index 0000000000..e568f4120a --- /dev/null +++ b/docs/docs/en/api/faststream/redis/publisher/factory/create_publisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.publisher.factory.create_publisher diff --git a/docs/docs/en/api/faststream/redis/publisher/fake/RedisFakePublisher.md b/docs/docs/en/api/faststream/redis/publisher/fake/RedisFakePublisher.md new file mode 100644 index 0000000000..eb00559657 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/publisher/fake/RedisFakePublisher.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.publisher.fake.RedisFakePublisher diff --git a/docs/docs/en/api/faststream/redis/publisher/specification/ChannelPublisherSpecification.md b/docs/docs/en/api/faststream/redis/publisher/specification/ChannelPublisherSpecification.md new file mode 100644 index 0000000000..5b9943850e --- /dev/null +++ b/docs/docs/en/api/faststream/redis/publisher/specification/ChannelPublisherSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.publisher.specification.ChannelPublisherSpecification diff --git a/docs/docs/en/api/faststream/redis/publisher/specification/ListPublisherSpecification.md b/docs/docs/en/api/faststream/redis/publisher/specification/ListPublisherSpecification.md new file mode 100644 index 0000000000..6b18a22ee6 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/publisher/specification/ListPublisherSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.publisher.specification.ListPublisherSpecification diff --git a/docs/docs/en/api/faststream/redis/publisher/specification/RedisPublisherSpecification.md b/docs/docs/en/api/faststream/redis/publisher/specification/RedisPublisherSpecification.md new file mode 100644 index 0000000000..b0a4bae770 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/publisher/specification/RedisPublisherSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.publisher.specification.RedisPublisherSpecification diff --git a/docs/docs/en/api/faststream/redis/publisher/specification/StreamPublisherSpecification.md b/docs/docs/en/api/faststream/redis/publisher/specification/StreamPublisherSpecification.md new file mode 100644 index 0000000000..2c4f6b07b5 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/publisher/specification/StreamPublisherSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.publisher.specification.StreamPublisherSpecification diff --git a/docs/docs/en/api/faststream/redis/response/DestinationType.md b/docs/docs/en/api/faststream/redis/response/DestinationType.md new file mode 100644 index 0000000000..4eda1ad154 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/response/DestinationType.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.response.DestinationType diff --git a/docs/docs/en/api/faststream/redis/response/RedisPublishCommand.md b/docs/docs/en/api/faststream/redis/response/RedisPublishCommand.md new file mode 100644 index 0000000000..14e21c799e --- /dev/null +++ b/docs/docs/en/api/faststream/redis/response/RedisPublishCommand.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.response.RedisPublishCommand diff --git a/docs/docs/en/api/faststream/redis/router/RedisPublisher.md b/docs/docs/en/api/faststream/redis/router/RedisPublisher.md deleted file mode 100644 index fd1cad4d37..0000000000 --- a/docs/docs/en/api/faststream/redis/router/RedisPublisher.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.router.RedisPublisher diff --git a/docs/docs/en/api/faststream/redis/router/RedisRoute.md b/docs/docs/en/api/faststream/redis/router/RedisRoute.md deleted file mode 100644 index d6e1f525a7..0000000000 --- a/docs/docs/en/api/faststream/redis/router/RedisRoute.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.router.RedisRoute diff --git a/docs/docs/en/api/faststream/redis/router/RedisRouter.md b/docs/docs/en/api/faststream/redis/router/RedisRouter.md deleted file mode 100644 index 373ceea5a8..0000000000 --- a/docs/docs/en/api/faststream/redis/router/RedisRouter.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.router.RedisRouter diff --git a/docs/docs/en/api/faststream/redis/schemas/proto/RedisAsyncAPIProtocol.md b/docs/docs/en/api/faststream/redis/schemas/proto/RedisAsyncAPIProtocol.md deleted file mode 100644 index 7a9d46c451..0000000000 --- a/docs/docs/en/api/faststream/redis/schemas/proto/RedisAsyncAPIProtocol.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.schemas.proto.RedisAsyncAPIProtocol diff --git a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIChannelSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIChannelSubscriber.md deleted file mode 100644 index 7cb7260111..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIChannelSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.asyncapi.AsyncAPIChannelSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIListBatchSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIListBatchSubscriber.md deleted file mode 100644 index 26aa621262..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIListBatchSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.asyncapi.AsyncAPIListBatchSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIListSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIListSubscriber.md deleted file mode 100644 index c65ba472d5..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIListSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.asyncapi.AsyncAPIListSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIStreamBatchSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIStreamBatchSubscriber.md deleted file mode 100644 index 099f0a4ff2..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIStreamBatchSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.asyncapi.AsyncAPIStreamBatchSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIStreamSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIStreamSubscriber.md deleted file mode 100644 index 3d85ce9587..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPIStreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.asyncapi.AsyncAPIStreamSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPISubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPISubscriber.md deleted file mode 100644 index c957f32688..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/asyncapi/AsyncAPISubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.asyncapi.AsyncAPISubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/config/RedisSubscriberConfig.md b/docs/docs/en/api/faststream/redis/subscriber/config/RedisSubscriberConfig.md new file mode 100644 index 0000000000..1e519d839a --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/config/RedisSubscriberConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.config.RedisSubscriberConfig diff --git a/docs/docs/en/api/faststream/redis/subscriber/config/RedisSubscriberSpecificationConfig.md b/docs/docs/en/api/faststream/redis/subscriber/config/RedisSubscriberSpecificationConfig.md new file mode 100644 index 0000000000..e31ea37fa3 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/config/RedisSubscriberSpecificationConfig.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.config.RedisSubscriberSpecificationConfig diff --git a/docs/docs/en/api/faststream/redis/subscriber/specification/ChannelSubscriberSpecification.md b/docs/docs/en/api/faststream/redis/subscriber/specification/ChannelSubscriberSpecification.md new file mode 100644 index 0000000000..1f96a1a836 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/specification/ChannelSubscriberSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.specification.ChannelSubscriberSpecification diff --git a/docs/docs/en/api/faststream/redis/subscriber/specification/ListSubscriberSpecification.md b/docs/docs/en/api/faststream/redis/subscriber/specification/ListSubscriberSpecification.md new file mode 100644 index 0000000000..0825f360df --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/specification/ListSubscriberSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.specification.ListSubscriberSpecification diff --git a/docs/docs/en/api/faststream/redis/subscriber/specification/RedisSubscriberSpecification.md b/docs/docs/en/api/faststream/redis/subscriber/specification/RedisSubscriberSpecification.md new file mode 100644 index 0000000000..f648611186 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/specification/RedisSubscriberSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.specification.RedisSubscriberSpecification diff --git a/docs/docs/en/api/faststream/redis/subscriber/specification/StreamSubscriberSpecification.md b/docs/docs/en/api/faststream/redis/subscriber/specification/StreamSubscriberSpecification.md new file mode 100644 index 0000000000..8ff056654a --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/specification/StreamSubscriberSpecification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.specification.StreamSubscriberSpecification diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecase/BatchListSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecase/BatchListSubscriber.md deleted file mode 100644 index aee1b8aa9b..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/usecase/BatchListSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.usecase.BatchListSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecase/BatchStreamSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecase/BatchStreamSubscriber.md deleted file mode 100644 index 0f8e4f2e1b..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/usecase/BatchStreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.usecase.BatchStreamSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecase/ChannelSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecase/ChannelSubscriber.md deleted file mode 100644 index 3ab1fc045a..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/usecase/ChannelSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.usecase.ChannelSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecase/ListSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecase/ListSubscriber.md deleted file mode 100644 index f7c44e8be5..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/usecase/ListSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.usecase.ListSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecase/LogicSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecase/LogicSubscriber.md deleted file mode 100644 index e3531e7dcc..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/usecase/LogicSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.usecase.LogicSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecase/StreamSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecase/StreamSubscriber.md deleted file mode 100644 index 6e2ac31d7f..0000000000 --- a/docs/docs/en/api/faststream/redis/subscriber/usecase/StreamSubscriber.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.redis.subscriber.usecase.StreamSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/ChannelConcurrentSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/ChannelConcurrentSubscriber.md new file mode 100644 index 0000000000..589eb5cccb --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/ChannelConcurrentSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.ChannelConcurrentSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/ChannelSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/ChannelSubscriber.md new file mode 100644 index 0000000000..c5eb341a8f --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/ChannelSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.ChannelSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/ListBatchSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/ListBatchSubscriber.md new file mode 100644 index 0000000000..0f6a343219 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/ListBatchSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.ListBatchSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/ListConcurrentSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/ListConcurrentSubscriber.md new file mode 100644 index 0000000000..58409c09cb --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/ListConcurrentSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.ListConcurrentSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/ListSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/ListSubscriber.md new file mode 100644 index 0000000000..552e758f74 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/ListSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.ListSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/LogicSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/LogicSubscriber.md new file mode 100644 index 0000000000..e6ad8c39bb --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/LogicSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.LogicSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/StreamBatchSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/StreamBatchSubscriber.md new file mode 100644 index 0000000000..c9d00dc5f0 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/StreamBatchSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.StreamBatchSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/StreamConcurrentSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/StreamConcurrentSubscriber.md new file mode 100644 index 0000000000..9ecc8d35ec --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/StreamConcurrentSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.StreamConcurrentSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/StreamSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/StreamSubscriber.md new file mode 100644 index 0000000000..dba098b2b2 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/StreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.StreamSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/basic/ConcurrentSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/basic/ConcurrentSubscriber.md new file mode 100644 index 0000000000..d43d3b8450 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/basic/ConcurrentSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.basic.ConcurrentSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/basic/LogicSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/basic/LogicSubscriber.md new file mode 100644 index 0000000000..5087fd0755 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/basic/LogicSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.basic.LogicSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/channel_subscriber/ChannelConcurrentSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/channel_subscriber/ChannelConcurrentSubscriber.md new file mode 100644 index 0000000000..2f40e7a8ed --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/channel_subscriber/ChannelConcurrentSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.channel_subscriber.ChannelConcurrentSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/channel_subscriber/ChannelSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/channel_subscriber/ChannelSubscriber.md new file mode 100644 index 0000000000..5b8fc53a23 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/channel_subscriber/ChannelSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.channel_subscriber.ChannelSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/list_subscriber/ListBatchSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/list_subscriber/ListBatchSubscriber.md new file mode 100644 index 0000000000..46eed7aa61 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/list_subscriber/ListBatchSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.list_subscriber.ListBatchSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/list_subscriber/ListConcurrentSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/list_subscriber/ListConcurrentSubscriber.md new file mode 100644 index 0000000000..a9e8be6b63 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/list_subscriber/ListConcurrentSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.list_subscriber.ListConcurrentSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/list_subscriber/ListSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/list_subscriber/ListSubscriber.md new file mode 100644 index 0000000000..b253657800 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/list_subscriber/ListSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.list_subscriber.ListSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/stream_subscriber/StreamBatchSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/stream_subscriber/StreamBatchSubscriber.md new file mode 100644 index 0000000000..f12a4b326c --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/stream_subscriber/StreamBatchSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.stream_subscriber.StreamBatchSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/stream_subscriber/StreamConcurrentSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/stream_subscriber/StreamConcurrentSubscriber.md new file mode 100644 index 0000000000..96f815bba5 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/stream_subscriber/StreamConcurrentSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.stream_subscriber.StreamConcurrentSubscriber diff --git a/docs/docs/en/api/faststream/redis/subscriber/usecases/stream_subscriber/StreamSubscriber.md b/docs/docs/en/api/faststream/redis/subscriber/usecases/stream_subscriber/StreamSubscriber.md new file mode 100644 index 0000000000..68d07e3324 --- /dev/null +++ b/docs/docs/en/api/faststream/redis/subscriber/usecases/stream_subscriber/StreamSubscriber.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.redis.subscriber.usecases.stream_subscriber.StreamSubscriber diff --git a/docs/docs/en/api/faststream/response/BatchPublishCommand.md b/docs/docs/en/api/faststream/response/BatchPublishCommand.md new file mode 100644 index 0000000000..386d213a47 --- /dev/null +++ b/docs/docs/en/api/faststream/response/BatchPublishCommand.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.response.BatchPublishCommand diff --git a/docs/docs/en/api/faststream/response/PublishCommand.md b/docs/docs/en/api/faststream/response/PublishCommand.md new file mode 100644 index 0000000000..8ca17ac376 --- /dev/null +++ b/docs/docs/en/api/faststream/response/PublishCommand.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.response.PublishCommand diff --git a/docs/docs/en/api/faststream/response/PublishType.md b/docs/docs/en/api/faststream/response/PublishType.md new file mode 100644 index 0000000000..57d3cbddd7 --- /dev/null +++ b/docs/docs/en/api/faststream/response/PublishType.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.response.PublishType diff --git a/docs/docs/en/api/faststream/response/Response.md b/docs/docs/en/api/faststream/response/Response.md new file mode 100644 index 0000000000..e96fe35896 --- /dev/null +++ b/docs/docs/en/api/faststream/response/Response.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.response.Response diff --git a/docs/docs/en/api/faststream/response/ensure_response.md b/docs/docs/en/api/faststream/response/ensure_response.md new file mode 100644 index 0000000000..e55d638acf --- /dev/null +++ b/docs/docs/en/api/faststream/response/ensure_response.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.response.ensure_response diff --git a/docs/docs/en/api/faststream/response/publish_type/PublishType.md b/docs/docs/en/api/faststream/response/publish_type/PublishType.md new file mode 100644 index 0000000000..2ac2fcd51c --- /dev/null +++ b/docs/docs/en/api/faststream/response/publish_type/PublishType.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.response.publish_type.PublishType diff --git a/docs/docs/en/api/faststream/response/response/BatchPublishCommand.md b/docs/docs/en/api/faststream/response/response/BatchPublishCommand.md new file mode 100644 index 0000000000..0f30558481 --- /dev/null +++ b/docs/docs/en/api/faststream/response/response/BatchPublishCommand.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.response.response.BatchPublishCommand diff --git a/docs/docs/en/api/faststream/response/response/PublishCommand.md b/docs/docs/en/api/faststream/response/response/PublishCommand.md new file mode 100644 index 0000000000..b247a7e5d8 --- /dev/null +++ b/docs/docs/en/api/faststream/response/response/PublishCommand.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.response.response.PublishCommand diff --git a/docs/docs/en/api/faststream/response/response/Response.md b/docs/docs/en/api/faststream/response/response/Response.md new file mode 100644 index 0000000000..01a68bb486 --- /dev/null +++ b/docs/docs/en/api/faststream/response/response/Response.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.response.response.Response diff --git a/docs/docs/en/api/faststream/response/utils/ensure_response.md b/docs/docs/en/api/faststream/response/utils/ensure_response.md new file mode 100644 index 0000000000..327b9ce951 --- /dev/null +++ b/docs/docs/en/api/faststream/response/utils/ensure_response.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.response.utils.ensure_response diff --git a/docs/docs/en/api/faststream/specification/AsyncAPI.md b/docs/docs/en/api/faststream/specification/AsyncAPI.md new file mode 100644 index 0000000000..4b23e3fa4a --- /dev/null +++ b/docs/docs/en/api/faststream/specification/AsyncAPI.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.AsyncAPI diff --git a/docs/docs/en/api/faststream/specification/Contact.md b/docs/docs/en/api/faststream/specification/Contact.md new file mode 100644 index 0000000000..aa8ac012ea --- /dev/null +++ b/docs/docs/en/api/faststream/specification/Contact.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.Contact diff --git a/docs/docs/en/api/faststream/specification/ExternalDocs.md b/docs/docs/en/api/faststream/specification/ExternalDocs.md new file mode 100644 index 0000000000..52e0432c94 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/ExternalDocs.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.ExternalDocs diff --git a/docs/docs/en/api/faststream/specification/License.md b/docs/docs/en/api/faststream/specification/License.md new file mode 100644 index 0000000000..ac2365f82b --- /dev/null +++ b/docs/docs/en/api/faststream/specification/License.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.License diff --git a/docs/docs/en/api/faststream/specification/Specification.md b/docs/docs/en/api/faststream/specification/Specification.md new file mode 100644 index 0000000000..c4cf4927aa --- /dev/null +++ b/docs/docs/en/api/faststream/specification/Specification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.Specification diff --git a/docs/docs/en/api/faststream/specification/Tag.md b/docs/docs/en/api/faststream/specification/Tag.md new file mode 100644 index 0000000000..ae4f1202a1 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/Tag.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.Tag diff --git a/docs/docs/en/api/faststream/specification/asyncapi/AsyncAPI.md b/docs/docs/en/api/faststream/specification/asyncapi/AsyncAPI.md new file mode 100644 index 0000000000..f6c0b2de09 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/AsyncAPI.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.AsyncAPI diff --git a/docs/docs/en/api/faststream/specification/asyncapi/factory/AsyncAPI.md b/docs/docs/en/api/faststream/specification/asyncapi/factory/AsyncAPI.md new file mode 100644 index 0000000000..9e68cc1f6c --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/factory/AsyncAPI.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.factory.AsyncAPI diff --git a/docs/docs/en/api/faststream/specification/asyncapi/get_asyncapi_html.md b/docs/docs/en/api/faststream/specification/asyncapi/get_asyncapi_html.md new file mode 100644 index 0000000000..02a5bf12cb --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/get_asyncapi_html.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.get_asyncapi_html diff --git a/docs/docs/en/api/faststream/specification/asyncapi/message/get_model_schema.md b/docs/docs/en/api/faststream/specification/asyncapi/message/get_model_schema.md new file mode 100644 index 0000000000..83b5c9026e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/message/get_model_schema.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.message.get_model_schema diff --git a/docs/docs/en/api/faststream/specification/asyncapi/message/get_response_schema.md b/docs/docs/en/api/faststream/specification/asyncapi/message/get_response_schema.md new file mode 100644 index 0000000000..d283b28902 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/message/get_response_schema.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.message.get_response_schema diff --git a/docs/docs/en/api/faststream/specification/asyncapi/message/parse_handler_params.md b/docs/docs/en/api/faststream/specification/asyncapi/message/parse_handler_params.md new file mode 100644 index 0000000000..cb6c4416f8 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/message/parse_handler_params.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.message.parse_handler_params diff --git a/docs/docs/en/api/faststream/specification/asyncapi/site/get_asyncapi_html.md b/docs/docs/en/api/faststream/specification/asyncapi/site/get_asyncapi_html.md new file mode 100644 index 0000000000..837b931af7 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/site/get_asyncapi_html.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.site.get_asyncapi_html diff --git a/docs/docs/en/api/faststream/specification/asyncapi/site/serve_app.md b/docs/docs/en/api/faststream/specification/asyncapi/site/serve_app.md new file mode 100644 index 0000000000..279e9eb8e0 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/site/serve_app.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.site.serve_app diff --git a/docs/docs/en/api/faststream/specification/asyncapi/utils/clear_key.md b/docs/docs/en/api/faststream/specification/asyncapi/utils/clear_key.md new file mode 100644 index 0000000000..cf6103d19a --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/utils/clear_key.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.utils.clear_key diff --git a/docs/docs/en/api/faststream/specification/asyncapi/utils/move_pydantic_refs.md b/docs/docs/en/api/faststream/specification/asyncapi/utils/move_pydantic_refs.md new file mode 100644 index 0000000000..445472f96d --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/utils/move_pydantic_refs.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.utils.move_pydantic_refs diff --git a/docs/docs/en/api/faststream/specification/asyncapi/utils/resolve_payloads.md b/docs/docs/en/api/faststream/specification/asyncapi/utils/resolve_payloads.md new file mode 100644 index 0000000000..d92d1850f3 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/utils/resolve_payloads.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.utils.resolve_payloads diff --git a/docs/docs/en/api/faststream/specification/asyncapi/utils/to_camelcase.md b/docs/docs/en/api/faststream/specification/asyncapi/utils/to_camelcase.md new file mode 100644 index 0000000000..5a260c9cca --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/utils/to_camelcase.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.utils.to_camelcase diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/AsyncAPI2.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/AsyncAPI2.md new file mode 100644 index 0000000000..b6ca4b870a --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/AsyncAPI2.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.AsyncAPI2 diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/facade/AsyncAPI2.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/facade/AsyncAPI2.md new file mode 100644 index 0000000000..1807a92056 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/facade/AsyncAPI2.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.facade.AsyncAPI2 diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/get_app_schema.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/get_app_schema.md new file mode 100644 index 0000000000..ef0e191d3d --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/get_app_schema.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.generate.get_app_schema diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/get_broker_channels.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/get_broker_channels.md new file mode 100644 index 0000000000..03fc069dc1 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/get_broker_channels.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.generate.get_broker_channels diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/get_broker_server.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/get_broker_server.md new file mode 100644 index 0000000000..c7b9007c14 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/get_broker_server.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.generate.get_broker_server diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/resolve_channel_messages.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/resolve_channel_messages.md new file mode 100644 index 0000000000..91abb99b8b --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/generate/resolve_channel_messages.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.generate.resolve_channel_messages diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/get_app_schema.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/get_app_schema.md new file mode 100644 index 0000000000..234b4b8bda --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/get_app_schema.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.get_app_schema diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ApplicationInfo.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ApplicationInfo.md new file mode 100644 index 0000000000..b7aaf16515 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ApplicationInfo.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.ApplicationInfo diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ApplicationSchema.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ApplicationSchema.md new file mode 100644 index 0000000000..614fed94e1 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ApplicationSchema.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.ApplicationSchema diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Channel.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Channel.md new file mode 100644 index 0000000000..76da65a973 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Channel.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.Channel diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Components.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Components.md new file mode 100644 index 0000000000..12ba1a0a30 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Components.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.Components diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Contact.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Contact.md new file mode 100644 index 0000000000..7eba14d8b1 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Contact.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.Contact diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/CorrelationId.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/CorrelationId.md new file mode 100644 index 0000000000..3173309f51 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/CorrelationId.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.CorrelationId diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ExternalDocs.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ExternalDocs.md new file mode 100644 index 0000000000..2df43d1016 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ExternalDocs.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.ExternalDocs diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/License.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/License.md new file mode 100644 index 0000000000..3f17b11448 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/License.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.License diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Message.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Message.md new file mode 100644 index 0000000000..f6247931d4 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Message.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.Message diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Operation.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Operation.md new file mode 100644 index 0000000000..e25d0e8ff2 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Operation.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.Operation diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Parameter.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Parameter.md new file mode 100644 index 0000000000..fac8aa5ee7 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Parameter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.Parameter diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Reference.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Reference.md new file mode 100644 index 0000000000..c469faf891 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Reference.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.Reference diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Server.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Server.md new file mode 100644 index 0000000000..82bc1ddb32 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Server.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.Server diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ServerVariable.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ServerVariable.md new file mode 100644 index 0000000000..06639f3ec5 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/ServerVariable.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.ServerVariable diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Tag.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Tag.md new file mode 100644 index 0000000000..35baa89db7 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/Tag.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.Tag diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/ChannelBinding.md new file mode 100644 index 0000000000..36874fc37c --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/OperationBinding.md new file mode 100644 index 0000000000..eb11d6b550 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/ChannelBinding.md new file mode 100644 index 0000000000..263e89f51b --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/OperationBinding.md new file mode 100644 index 0000000000..350a96b413 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/ChannelBinding.md new file mode 100644 index 0000000000..6b4f8d6d93 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/Exchange.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/Exchange.md new file mode 100644 index 0000000000..dbd3288b64 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/Exchange.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp.channel.Exchange diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/Queue.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/Queue.md new file mode 100644 index 0000000000..31de312529 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel/Queue.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp.channel.Queue diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/operation/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/operation/OperationBinding.md new file mode 100644 index 0000000000..937cda8820 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/operation/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/ChannelBinding.md new file mode 100644 index 0000000000..7792a2bef7 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.kafka.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/OperationBinding.md new file mode 100644 index 0000000000..21c35af99f --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.kafka.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/channel/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/channel/ChannelBinding.md new file mode 100644 index 0000000000..82b2556711 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/channel/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.kafka.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/operation/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/operation/OperationBinding.md new file mode 100644 index 0000000000..49e28f86ef --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/operation/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.kafka.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/ChannelBinding.md new file mode 100644 index 0000000000..a044af926c --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.main.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/OperationBinding.md new file mode 100644 index 0000000000..9cf2d46f59 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.main.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/channel/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/channel/ChannelBinding.md new file mode 100644 index 0000000000..c7d4ecef03 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/channel/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.main.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/operation/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/operation/OperationBinding.md new file mode 100644 index 0000000000..b464bbb356 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/operation/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.main.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/ChannelBinding.md new file mode 100644 index 0000000000..8380c4571d --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.nats.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/OperationBinding.md new file mode 100644 index 0000000000..c48ff3bad7 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.nats.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/channel/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/channel/ChannelBinding.md new file mode 100644 index 0000000000..ebc06a51b8 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/channel/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.nats.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/operation/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/operation/OperationBinding.md new file mode 100644 index 0000000000..10ebc684ce --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/operation/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.nats.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/ChannelBinding.md new file mode 100644 index 0000000000..49402a3533 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.redis.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/OperationBinding.md new file mode 100644 index 0000000000..4a41fede12 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.redis.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/channel/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/channel/ChannelBinding.md new file mode 100644 index 0000000000..4c859b133e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/channel/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.redis.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/operation/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/operation/OperationBinding.md new file mode 100644 index 0000000000..5e7ef90817 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/operation/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.redis.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/ChannelBinding.md new file mode 100644 index 0000000000..efd54e587e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.sqs.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/OperationBinding.md new file mode 100644 index 0000000000..4f3babf6a5 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.sqs.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/channel/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/channel/ChannelBinding.md new file mode 100644 index 0000000000..274f800ea8 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/channel/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.sqs.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/operation/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/operation/OperationBinding.md new file mode 100644 index 0000000000..931067bfef --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/operation/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.sqs.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/channels/Channel.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/channels/Channel.md new file mode 100644 index 0000000000..520d156b96 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/channels/Channel.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.channels.Channel diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/components/Components.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/components/Components.md new file mode 100644 index 0000000000..215edc3e69 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/components/Components.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.components.Components diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/contact/Contact.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/contact/Contact.md new file mode 100644 index 0000000000..14b7574d92 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/contact/Contact.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.contact.Contact diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/docs/ExternalDocs.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/docs/ExternalDocs.md new file mode 100644 index 0000000000..8b66a2c5f3 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/docs/ExternalDocs.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.docs.ExternalDocs diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/info/ApplicationInfo.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/info/ApplicationInfo.md new file mode 100644 index 0000000000..db377ddd61 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/info/ApplicationInfo.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.info.ApplicationInfo diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/license/License.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/license/License.md new file mode 100644 index 0000000000..d9d4102522 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/license/License.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.license.License diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/message/CorrelationId.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/message/CorrelationId.md new file mode 100644 index 0000000000..bbfc2f40d8 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/message/CorrelationId.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.message.CorrelationId diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/message/Message.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/message/Message.md new file mode 100644 index 0000000000..9667e77e8e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/message/Message.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.message.Message diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/operations/Operation.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/operations/Operation.md new file mode 100644 index 0000000000..3d750e5f3e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/operations/Operation.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.operations.Operation diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/schema/ApplicationSchema.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/schema/ApplicationSchema.md new file mode 100644 index 0000000000..2a380a8e4d --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/schema/ApplicationSchema.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.schema.ApplicationSchema diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/servers/Server.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/servers/Server.md new file mode 100644 index 0000000000..a50c1a5cf1 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/servers/Server.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.servers.Server diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/servers/ServerVariable.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/servers/ServerVariable.md new file mode 100644 index 0000000000..8606288a32 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/servers/ServerVariable.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.servers.ServerVariable diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/tag/Tag.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/tag/Tag.md new file mode 100644 index 0000000000..c3d9025966 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/tag/Tag.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.tag.Tag diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/utils/Parameter.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/utils/Parameter.md new file mode 100644 index 0000000000..c2971d5485 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/utils/Parameter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.utils.Parameter diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/utils/Reference.md b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/utils/Reference.md new file mode 100644 index 0000000000..2737907b0e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v2_6_0/schema/utils/Reference.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.utils.Reference diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/AsyncAPI3.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/AsyncAPI3.md new file mode 100644 index 0000000000..4eb400a450 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/AsyncAPI3.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.AsyncAPI3 diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/facade/AsyncAPI3.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/facade/AsyncAPI3.md new file mode 100644 index 0000000000..5216f86a02 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/facade/AsyncAPI3.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.facade.AsyncAPI3 diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/generate/get_app_schema.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/generate/get_app_schema.md new file mode 100644 index 0000000000..0faa97ba94 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/generate/get_app_schema.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.generate.get_app_schema diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/generate/get_broker_channels.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/generate/get_broker_channels.md new file mode 100644 index 0000000000..d43691125d --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/generate/get_broker_channels.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.generate.get_broker_channels diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/generate/get_broker_server.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/generate/get_broker_server.md new file mode 100644 index 0000000000..1c71db1292 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/generate/get_broker_server.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.generate.get_broker_server diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/get_app_schema.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/get_app_schema.md new file mode 100644 index 0000000000..977c46289d --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/get_app_schema.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.get_app_schema diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ApplicationInfo.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ApplicationInfo.md new file mode 100644 index 0000000000..be7a85513c --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ApplicationInfo.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.ApplicationInfo diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ApplicationSchema.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ApplicationSchema.md new file mode 100644 index 0000000000..0f3fa28a38 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ApplicationSchema.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.ApplicationSchema diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Channel.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Channel.md new file mode 100644 index 0000000000..f213110586 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Channel.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.Channel diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Components.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Components.md new file mode 100644 index 0000000000..bce8d6e93e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Components.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.Components diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Contact.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Contact.md new file mode 100644 index 0000000000..14b7574d92 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Contact.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.contact.Contact diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/CorrelationId.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/CorrelationId.md new file mode 100644 index 0000000000..bbfc2f40d8 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/CorrelationId.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.message.CorrelationId diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ExternalDocs.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ExternalDocs.md new file mode 100644 index 0000000000..8b66a2c5f3 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ExternalDocs.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.docs.ExternalDocs diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/License.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/License.md new file mode 100644 index 0000000000..d9d4102522 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/License.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.license.License diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Message.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Message.md new file mode 100644 index 0000000000..9667e77e8e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Message.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.message.Message diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Operation.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Operation.md new file mode 100644 index 0000000000..a46ecefb8d --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Operation.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.Operation diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Parameter.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Parameter.md new file mode 100644 index 0000000000..c2971d5485 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Parameter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.utils.Parameter diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Reference.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Reference.md new file mode 100644 index 0000000000..2737907b0e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Reference.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.utils.Reference diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Server.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Server.md new file mode 100644 index 0000000000..d41ebe84c3 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Server.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.Server diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ServerVariable.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ServerVariable.md new file mode 100644 index 0000000000..8606288a32 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/ServerVariable.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.servers.ServerVariable diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Tag.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Tag.md new file mode 100644 index 0000000000..c3d9025966 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/Tag.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.tag.Tag diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/ChannelBinding.md new file mode 100644 index 0000000000..ca8431218b --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.bindings.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/OperationBinding.md new file mode 100644 index 0000000000..876f866f35 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.bindings.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/ChannelBinding.md new file mode 100644 index 0000000000..2777001c4d --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.bindings.amqp.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/OperationBinding.md new file mode 100644 index 0000000000..325bba3f9e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.bindings.amqp.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/channel/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/channel/ChannelBinding.md new file mode 100644 index 0000000000..74c10098ac --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/channel/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.bindings.amqp.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/operation/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/operation/OperationBinding.md new file mode 100644 index 0000000000..51c5024cc7 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/operation/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.bindings.amqp.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/kafka/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/kafka/ChannelBinding.md new file mode 100644 index 0000000000..82b2556711 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/kafka/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.kafka.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/kafka/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/kafka/OperationBinding.md new file mode 100644 index 0000000000..49e28f86ef --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/kafka/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.kafka.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/ChannelBinding.md new file mode 100644 index 0000000000..07977ed15f --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.bindings.main.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/OperationBinding.md new file mode 100644 index 0000000000..c897790951 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.bindings.main.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/channel/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/channel/ChannelBinding.md new file mode 100644 index 0000000000..409e449a27 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/channel/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.bindings.main.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/operation/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/operation/OperationBinding.md new file mode 100644 index 0000000000..4e85a7f5e4 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/operation/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.bindings.main.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/nats/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/nats/ChannelBinding.md new file mode 100644 index 0000000000..ebc06a51b8 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/nats/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.nats.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/nats/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/nats/OperationBinding.md new file mode 100644 index 0000000000..10ebc684ce --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/nats/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.nats.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/redis/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/redis/ChannelBinding.md new file mode 100644 index 0000000000..4c859b133e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/redis/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.redis.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/redis/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/redis/OperationBinding.md new file mode 100644 index 0000000000..5e7ef90817 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/redis/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.redis.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/sqs/ChannelBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/sqs/ChannelBinding.md new file mode 100644 index 0000000000..274f800ea8 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/sqs/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.sqs.channel.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/sqs/OperationBinding.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/sqs/OperationBinding.md new file mode 100644 index 0000000000..931067bfef --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/bindings/sqs/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.bindings.sqs.operation.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/channels/Channel.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/channels/Channel.md new file mode 100644 index 0000000000..b3b425a350 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/channels/Channel.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.channels.Channel diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/components/Components.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/components/Components.md new file mode 100644 index 0000000000..db5c489f43 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/components/Components.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.components.Components diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/contact/Contact.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/contact/Contact.md new file mode 100644 index 0000000000..14b7574d92 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/contact/Contact.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.contact.Contact diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/docs/ExternalDocs.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/docs/ExternalDocs.md new file mode 100644 index 0000000000..8b66a2c5f3 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/docs/ExternalDocs.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.docs.ExternalDocs diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/info/ApplicationInfo.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/info/ApplicationInfo.md new file mode 100644 index 0000000000..f0c2b0f95d --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/info/ApplicationInfo.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.info.ApplicationInfo diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/license/License.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/license/License.md new file mode 100644 index 0000000000..d9d4102522 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/license/License.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.license.License diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/message/CorrelationId.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/message/CorrelationId.md new file mode 100644 index 0000000000..bbfc2f40d8 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/message/CorrelationId.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.message.CorrelationId diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/message/Message.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/message/Message.md new file mode 100644 index 0000000000..9667e77e8e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/message/Message.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.message.Message diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/operations/Action.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/operations/Action.md new file mode 100644 index 0000000000..c5a9956348 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/operations/Action.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.operations.Action diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/operations/Operation.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/operations/Operation.md new file mode 100644 index 0000000000..1ac73f73da --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/operations/Operation.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.operations.Operation diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/schema/ApplicationSchema.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/schema/ApplicationSchema.md new file mode 100644 index 0000000000..57fc00568a --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/schema/ApplicationSchema.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.schema.ApplicationSchema diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/servers/Server.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/servers/Server.md new file mode 100644 index 0000000000..1d9f1168d9 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/servers/Server.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v3_0_0.schema.servers.Server diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/servers/ServerVariable.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/servers/ServerVariable.md new file mode 100644 index 0000000000..8606288a32 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/servers/ServerVariable.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.servers.ServerVariable diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/tag/Tag.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/tag/Tag.md new file mode 100644 index 0000000000..c3d9025966 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/tag/Tag.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.tag.Tag diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/utils/Parameter.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/utils/Parameter.md new file mode 100644 index 0000000000..c2971d5485 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/utils/Parameter.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.utils.Parameter diff --git a/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/utils/Reference.md b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/utils/Reference.md new file mode 100644 index 0000000000..2737907b0e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/asyncapi/v3_0_0/schema/utils/Reference.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.asyncapi.v2_6_0.schema.utils.Reference diff --git a/docs/docs/en/api/faststream/specification/base/info/BaseApplicationInfo.md b/docs/docs/en/api/faststream/specification/base/info/BaseApplicationInfo.md new file mode 100644 index 0000000000..324bbbcec9 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/base/info/BaseApplicationInfo.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.base.info.BaseApplicationInfo diff --git a/docs/docs/en/api/faststream/specification/base/schema/BaseApplicationSchema.md b/docs/docs/en/api/faststream/specification/base/schema/BaseApplicationSchema.md new file mode 100644 index 0000000000..8d9291322c --- /dev/null +++ b/docs/docs/en/api/faststream/specification/base/schema/BaseApplicationSchema.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.base.schema.BaseApplicationSchema diff --git a/docs/docs/en/api/faststream/specification/base/specification/Specification.md b/docs/docs/en/api/faststream/specification/base/specification/Specification.md new file mode 100644 index 0000000000..0b3f07b9f7 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/base/specification/Specification.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.base.specification.Specification diff --git a/docs/docs/en/api/faststream/specification/schema/BrokerSpec.md b/docs/docs/en/api/faststream/specification/schema/BrokerSpec.md new file mode 100644 index 0000000000..2e503a1a76 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/BrokerSpec.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.BrokerSpec diff --git a/docs/docs/en/api/faststream/specification/schema/Contact.md b/docs/docs/en/api/faststream/specification/schema/Contact.md new file mode 100644 index 0000000000..5c95b1d99b --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/Contact.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.Contact diff --git a/docs/docs/en/api/faststream/specification/schema/ContactDict.md b/docs/docs/en/api/faststream/specification/schema/ContactDict.md new file mode 100644 index 0000000000..79d9ca5fb2 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/ContactDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.ContactDict diff --git a/docs/docs/en/api/faststream/specification/schema/ExternalDocs.md b/docs/docs/en/api/faststream/specification/schema/ExternalDocs.md new file mode 100644 index 0000000000..242418f578 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/ExternalDocs.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.ExternalDocs diff --git a/docs/docs/en/api/faststream/specification/schema/ExternalDocsDict.md b/docs/docs/en/api/faststream/specification/schema/ExternalDocsDict.md new file mode 100644 index 0000000000..788848b13a --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/ExternalDocsDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.ExternalDocsDict diff --git a/docs/docs/en/api/faststream/specification/schema/License.md b/docs/docs/en/api/faststream/specification/schema/License.md new file mode 100644 index 0000000000..4e321c7aab --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/License.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.License diff --git a/docs/docs/en/api/faststream/specification/schema/LicenseDict.md b/docs/docs/en/api/faststream/specification/schema/LicenseDict.md new file mode 100644 index 0000000000..16204a3fc8 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/LicenseDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.LicenseDict diff --git a/docs/docs/en/api/faststream/specification/schema/Message.md b/docs/docs/en/api/faststream/specification/schema/Message.md new file mode 100644 index 0000000000..d1baee52f5 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/Message.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.Message diff --git a/docs/docs/en/api/faststream/specification/schema/Operation.md b/docs/docs/en/api/faststream/specification/schema/Operation.md new file mode 100644 index 0000000000..cde09d402f --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/Operation.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.Operation diff --git a/docs/docs/en/api/faststream/specification/schema/PublisherSpec.md b/docs/docs/en/api/faststream/specification/schema/PublisherSpec.md new file mode 100644 index 0000000000..5b3e7f23e5 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/PublisherSpec.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.PublisherSpec diff --git a/docs/docs/en/api/faststream/specification/schema/SubscriberSpec.md b/docs/docs/en/api/faststream/specification/schema/SubscriberSpec.md new file mode 100644 index 0000000000..7ec1f6964b --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/SubscriberSpec.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.SubscriberSpec diff --git a/docs/docs/en/api/faststream/specification/schema/Tag.md b/docs/docs/en/api/faststream/specification/schema/Tag.md new file mode 100644 index 0000000000..03071c314f --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/Tag.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.Tag diff --git a/docs/docs/en/api/faststream/specification/schema/TagDict.md b/docs/docs/en/api/faststream/specification/schema/TagDict.md new file mode 100644 index 0000000000..02f4de9c05 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/TagDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.TagDict diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/ChannelBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/ChannelBinding.md new file mode 100644 index 0000000000..cb7f566dc1 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/OperationBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/OperationBinding.md new file mode 100644 index 0000000000..6fcad5ea7e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/amqp/ChannelBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/amqp/ChannelBinding.md new file mode 100644 index 0000000000..194c68f536 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/amqp/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.amqp.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/amqp/Exchange.md b/docs/docs/en/api/faststream/specification/schema/bindings/amqp/Exchange.md new file mode 100644 index 0000000000..355222c2bb --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/amqp/Exchange.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.amqp.Exchange diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/amqp/OperationBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/amqp/OperationBinding.md new file mode 100644 index 0000000000..6bb90ca8ae --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/amqp/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.amqp.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/amqp/Queue.md b/docs/docs/en/api/faststream/specification/schema/bindings/amqp/Queue.md new file mode 100644 index 0000000000..54c9493d0e --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/amqp/Queue.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.amqp.Queue diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/http/OperationBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/http/OperationBinding.md new file mode 100644 index 0000000000..0eddf67257 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/http/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.http.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/kafka/ChannelBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/kafka/ChannelBinding.md new file mode 100644 index 0000000000..2561bf2e72 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/kafka/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.kafka.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/kafka/OperationBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/kafka/OperationBinding.md new file mode 100644 index 0000000000..0746cedd33 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/kafka/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.kafka.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/main/ChannelBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/main/ChannelBinding.md new file mode 100644 index 0000000000..73bfc4bb40 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/main/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.main.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/main/OperationBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/main/OperationBinding.md new file mode 100644 index 0000000000..f4ebb70b9c --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/main/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.main.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/nats/ChannelBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/nats/ChannelBinding.md new file mode 100644 index 0000000000..4495e21ac2 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/nats/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.nats.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/nats/OperationBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/nats/OperationBinding.md new file mode 100644 index 0000000000..fde8061b86 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/nats/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.nats.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/redis/ChannelBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/redis/ChannelBinding.md new file mode 100644 index 0000000000..0f991824da --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/redis/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.redis.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/redis/OperationBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/redis/OperationBinding.md new file mode 100644 index 0000000000..95e3ca446a --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/redis/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.redis.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/sqs/ChannelBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/sqs/ChannelBinding.md new file mode 100644 index 0000000000..521a6f5560 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/sqs/ChannelBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.sqs.ChannelBinding diff --git a/docs/docs/en/api/faststream/specification/schema/bindings/sqs/OperationBinding.md b/docs/docs/en/api/faststream/specification/schema/bindings/sqs/OperationBinding.md new file mode 100644 index 0000000000..a70995cb7f --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/bindings/sqs/OperationBinding.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.bindings.sqs.OperationBinding diff --git a/docs/docs/en/api/faststream/specification/schema/broker/BrokerSpec.md b/docs/docs/en/api/faststream/specification/schema/broker/BrokerSpec.md new file mode 100644 index 0000000000..262f3728f4 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/broker/BrokerSpec.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.broker.BrokerSpec diff --git a/docs/docs/en/api/faststream/specification/schema/extra/Contact.md b/docs/docs/en/api/faststream/specification/schema/extra/Contact.md new file mode 100644 index 0000000000..f89be2dc65 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/Contact.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.Contact diff --git a/docs/docs/en/api/faststream/specification/schema/extra/ContactDict.md b/docs/docs/en/api/faststream/specification/schema/extra/ContactDict.md new file mode 100644 index 0000000000..8f20fd396d --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/ContactDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.ContactDict diff --git a/docs/docs/en/api/faststream/specification/schema/extra/ExternalDocs.md b/docs/docs/en/api/faststream/specification/schema/extra/ExternalDocs.md new file mode 100644 index 0000000000..a745e15910 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/ExternalDocs.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.ExternalDocs diff --git a/docs/docs/en/api/faststream/specification/schema/extra/ExternalDocsDict.md b/docs/docs/en/api/faststream/specification/schema/extra/ExternalDocsDict.md new file mode 100644 index 0000000000..1110d88070 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/ExternalDocsDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.ExternalDocsDict diff --git a/docs/docs/en/api/faststream/specification/schema/extra/License.md b/docs/docs/en/api/faststream/specification/schema/extra/License.md new file mode 100644 index 0000000000..033374ed25 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/License.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.License diff --git a/docs/docs/en/api/faststream/specification/schema/extra/LicenseDict.md b/docs/docs/en/api/faststream/specification/schema/extra/LicenseDict.md new file mode 100644 index 0000000000..e2994e1d88 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/LicenseDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.LicenseDict diff --git a/docs/docs/en/api/faststream/specification/schema/extra/Tag.md b/docs/docs/en/api/faststream/specification/schema/extra/Tag.md new file mode 100644 index 0000000000..cc373f1528 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/Tag.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.Tag diff --git a/docs/docs/en/api/faststream/specification/schema/extra/TagDict.md b/docs/docs/en/api/faststream/specification/schema/extra/TagDict.md new file mode 100644 index 0000000000..c2455e5c4f --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/TagDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.TagDict diff --git a/docs/docs/en/api/faststream/specification/schema/extra/contact/Contact.md b/docs/docs/en/api/faststream/specification/schema/extra/contact/Contact.md new file mode 100644 index 0000000000..f34ea777d3 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/contact/Contact.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.contact.Contact diff --git a/docs/docs/en/api/faststream/specification/schema/extra/contact/ContactDict.md b/docs/docs/en/api/faststream/specification/schema/extra/contact/ContactDict.md new file mode 100644 index 0000000000..17d0d774eb --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/contact/ContactDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.contact.ContactDict diff --git a/docs/docs/en/api/faststream/specification/schema/extra/external_docs/ExternalDocs.md b/docs/docs/en/api/faststream/specification/schema/extra/external_docs/ExternalDocs.md new file mode 100644 index 0000000000..bf06e4b97c --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/external_docs/ExternalDocs.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.external_docs.ExternalDocs diff --git a/docs/docs/en/api/faststream/specification/schema/extra/external_docs/ExternalDocsDict.md b/docs/docs/en/api/faststream/specification/schema/extra/external_docs/ExternalDocsDict.md new file mode 100644 index 0000000000..cf1c76bc2b --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/external_docs/ExternalDocsDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.external_docs.ExternalDocsDict diff --git a/docs/docs/en/api/faststream/specification/schema/extra/license/License.md b/docs/docs/en/api/faststream/specification/schema/extra/license/License.md new file mode 100644 index 0000000000..58cb3821e7 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/license/License.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.license.License diff --git a/docs/docs/en/api/faststream/specification/schema/extra/license/LicenseDict.md b/docs/docs/en/api/faststream/specification/schema/extra/license/LicenseDict.md new file mode 100644 index 0000000000..fb8779aa39 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/license/LicenseDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.license.LicenseDict diff --git a/docs/docs/en/api/faststream/specification/schema/extra/tag/Tag.md b/docs/docs/en/api/faststream/specification/schema/extra/tag/Tag.md new file mode 100644 index 0000000000..b25a4d5cb1 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/tag/Tag.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.tag.Tag diff --git a/docs/docs/en/api/faststream/specification/schema/extra/tag/TagDict.md b/docs/docs/en/api/faststream/specification/schema/extra/tag/TagDict.md new file mode 100644 index 0000000000..92500cc5b2 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/extra/tag/TagDict.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.extra.tag.TagDict diff --git a/docs/docs/en/api/faststream/specification/schema/message/Message.md b/docs/docs/en/api/faststream/specification/schema/message/Message.md new file mode 100644 index 0000000000..1c2fd0ceb8 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/message/Message.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.message.Message diff --git a/docs/docs/en/api/faststream/specification/schema/message/model/Message.md b/docs/docs/en/api/faststream/specification/schema/message/model/Message.md new file mode 100644 index 0000000000..f057a4e898 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/message/model/Message.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.message.model.Message diff --git a/docs/docs/en/api/faststream/specification/schema/operation/Operation.md b/docs/docs/en/api/faststream/specification/schema/operation/Operation.md new file mode 100644 index 0000000000..23088a80f0 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/operation/Operation.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.operation.Operation diff --git a/docs/docs/en/api/faststream/specification/schema/operation/model/Operation.md b/docs/docs/en/api/faststream/specification/schema/operation/model/Operation.md new file mode 100644 index 0000000000..17c306a8ab --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/operation/model/Operation.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.operation.model.Operation diff --git a/docs/docs/en/api/faststream/specification/schema/publisher/PublisherSpec.md b/docs/docs/en/api/faststream/specification/schema/publisher/PublisherSpec.md new file mode 100644 index 0000000000..002bc1ecb8 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/publisher/PublisherSpec.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.publisher.PublisherSpec diff --git a/docs/docs/en/api/faststream/specification/schema/subscriber/SubscriberSpec.md b/docs/docs/en/api/faststream/specification/schema/subscriber/SubscriberSpec.md new file mode 100644 index 0000000000..5a4e4515e2 --- /dev/null +++ b/docs/docs/en/api/faststream/specification/schema/subscriber/SubscriberSpec.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: faststream.specification.schema.subscriber.SubscriberSpec diff --git a/docs/docs/en/api/faststream/testing/TestApp.md b/docs/docs/en/api/faststream/testing/TestApp.md deleted file mode 100644 index 3d8f650f0f..0000000000 --- a/docs/docs/en/api/faststream/testing/TestApp.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.testing.TestApp diff --git a/docs/docs/en/api/faststream/testing/app/TestApp.md b/docs/docs/en/api/faststream/testing/app/TestApp.md deleted file mode 100644 index 2468f3755c..0000000000 --- a/docs/docs/en/api/faststream/testing/app/TestApp.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.testing.app.TestApp diff --git a/docs/docs/en/api/faststream/testing/broker/TestBroker.md b/docs/docs/en/api/faststream/testing/broker/TestBroker.md deleted file mode 100644 index 48e34a6ca3..0000000000 --- a/docs/docs/en/api/faststream/testing/broker/TestBroker.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.testing.broker.TestBroker diff --git a/docs/docs/en/api/faststream/testing/broker/patch_broker_calls.md b/docs/docs/en/api/faststream/testing/broker/patch_broker_calls.md deleted file mode 100644 index 12a6431765..0000000000 --- a/docs/docs/en/api/faststream/testing/broker/patch_broker_calls.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.testing.broker.patch_broker_calls diff --git a/docs/docs/en/api/faststream/types/LoggerProto.md b/docs/docs/en/api/faststream/types/LoggerProto.md deleted file mode 100644 index 064320bf42..0000000000 --- a/docs/docs/en/api/faststream/types/LoggerProto.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.types.LoggerProto diff --git a/docs/docs/en/api/faststream/types/StandardDataclass.md b/docs/docs/en/api/faststream/types/StandardDataclass.md deleted file mode 100644 index 5140818794..0000000000 --- a/docs/docs/en/api/faststream/types/StandardDataclass.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.types.StandardDataclass diff --git a/docs/docs/en/api/faststream/utils/Context.md b/docs/docs/en/api/faststream/utils/Context.md deleted file mode 100644 index 3e4f9f17c5..0000000000 --- a/docs/docs/en/api/faststream/utils/Context.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.Context diff --git a/docs/docs/en/api/faststream/utils/ContextRepo.md b/docs/docs/en/api/faststream/utils/ContextRepo.md deleted file mode 100644 index dd18ad81e4..0000000000 --- a/docs/docs/en/api/faststream/utils/ContextRepo.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.ContextRepo diff --git a/docs/docs/en/api/faststream/utils/Header.md b/docs/docs/en/api/faststream/utils/Header.md deleted file mode 100644 index 10e6ccaec7..0000000000 --- a/docs/docs/en/api/faststream/utils/Header.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.Header diff --git a/docs/docs/en/api/faststream/utils/NoCast.md b/docs/docs/en/api/faststream/utils/NoCast.md deleted file mode 100644 index 606a31e563..0000000000 --- a/docs/docs/en/api/faststream/utils/NoCast.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.NoCast diff --git a/docs/docs/en/api/faststream/utils/Path.md b/docs/docs/en/api/faststream/utils/Path.md deleted file mode 100644 index b311930841..0000000000 --- a/docs/docs/en/api/faststream/utils/Path.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.Path diff --git a/docs/docs/en/api/faststream/utils/apply_types.md b/docs/docs/en/api/faststream/utils/apply_types.md deleted file mode 100644 index 9dc4603bd2..0000000000 --- a/docs/docs/en/api/faststream/utils/apply_types.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: fast_depends.use.inject diff --git a/docs/docs/en/api/faststream/utils/ast/find_ast_node.md b/docs/docs/en/api/faststream/utils/ast/find_ast_node.md deleted file mode 100644 index 228e6f058c..0000000000 --- a/docs/docs/en/api/faststream/utils/ast/find_ast_node.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.ast.find_ast_node diff --git a/docs/docs/en/api/faststream/utils/ast/find_withitems.md b/docs/docs/en/api/faststream/utils/ast/find_withitems.md deleted file mode 100644 index 123acd71e4..0000000000 --- a/docs/docs/en/api/faststream/utils/ast/find_withitems.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.ast.find_withitems diff --git a/docs/docs/en/api/faststream/utils/ast/get_withitem_calls.md b/docs/docs/en/api/faststream/utils/ast/get_withitem_calls.md deleted file mode 100644 index c9d68c1ed2..0000000000 --- a/docs/docs/en/api/faststream/utils/ast/get_withitem_calls.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.ast.get_withitem_calls diff --git a/docs/docs/en/api/faststream/utils/ast/is_contains_context_name.md b/docs/docs/en/api/faststream/utils/ast/is_contains_context_name.md deleted file mode 100644 index 61cf140ea6..0000000000 --- a/docs/docs/en/api/faststream/utils/ast/is_contains_context_name.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.ast.is_contains_context_name diff --git a/docs/docs/en/api/faststream/utils/classes/Singleton.md b/docs/docs/en/api/faststream/utils/classes/Singleton.md deleted file mode 100644 index c9751ee2bd..0000000000 --- a/docs/docs/en/api/faststream/utils/classes/Singleton.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.classes.Singleton diff --git a/docs/docs/en/api/faststream/utils/context/Context.md b/docs/docs/en/api/faststream/utils/context/Context.md deleted file mode 100644 index 5669863fee..0000000000 --- a/docs/docs/en/api/faststream/utils/context/Context.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.context.Context diff --git a/docs/docs/en/api/faststream/utils/context/ContextRepo.md b/docs/docs/en/api/faststream/utils/context/ContextRepo.md deleted file mode 100644 index 50a7133aeb..0000000000 --- a/docs/docs/en/api/faststream/utils/context/ContextRepo.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.context.ContextRepo diff --git a/docs/docs/en/api/faststream/utils/context/Header.md b/docs/docs/en/api/faststream/utils/context/Header.md deleted file mode 100644 index 7e10284ec1..0000000000 --- a/docs/docs/en/api/faststream/utils/context/Header.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.context.Header diff --git a/docs/docs/en/api/faststream/utils/context/Path.md b/docs/docs/en/api/faststream/utils/context/Path.md deleted file mode 100644 index 92c2ef36fe..0000000000 --- a/docs/docs/en/api/faststream/utils/context/Path.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.context.Path diff --git a/docs/docs/en/api/faststream/utils/context/builders/Context.md b/docs/docs/en/api/faststream/utils/context/builders/Context.md deleted file mode 100644 index 6cdf6f36fe..0000000000 --- a/docs/docs/en/api/faststream/utils/context/builders/Context.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.context.builders.Context diff --git a/docs/docs/en/api/faststream/utils/context/builders/Header.md b/docs/docs/en/api/faststream/utils/context/builders/Header.md deleted file mode 100644 index e3f6e41ba6..0000000000 --- a/docs/docs/en/api/faststream/utils/context/builders/Header.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.context.builders.Header diff --git a/docs/docs/en/api/faststream/utils/context/builders/Path.md b/docs/docs/en/api/faststream/utils/context/builders/Path.md deleted file mode 100644 index 5203903c45..0000000000 --- a/docs/docs/en/api/faststream/utils/context/builders/Path.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.context.builders.Path diff --git a/docs/docs/en/api/faststream/utils/context/repository/ContextRepo.md b/docs/docs/en/api/faststream/utils/context/repository/ContextRepo.md deleted file mode 100644 index ad968d8954..0000000000 --- a/docs/docs/en/api/faststream/utils/context/repository/ContextRepo.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.context.repository.ContextRepo diff --git a/docs/docs/en/api/faststream/utils/context/types/Context.md b/docs/docs/en/api/faststream/utils/context/types/Context.md deleted file mode 100644 index 3ac9c51fad..0000000000 --- a/docs/docs/en/api/faststream/utils/context/types/Context.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.context.types.Context diff --git a/docs/docs/en/api/faststream/utils/context/types/resolve_context_by_name.md b/docs/docs/en/api/faststream/utils/context/types/resolve_context_by_name.md deleted file mode 100644 index 60ab9fc23c..0000000000 --- a/docs/docs/en/api/faststream/utils/context/types/resolve_context_by_name.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.context.types.resolve_context_by_name diff --git a/docs/docs/en/api/faststream/utils/data/filter_by_dict.md b/docs/docs/en/api/faststream/utils/data/filter_by_dict.md deleted file mode 100644 index 87d03b5288..0000000000 --- a/docs/docs/en/api/faststream/utils/data/filter_by_dict.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.data.filter_by_dict diff --git a/docs/docs/en/api/faststream/utils/functions/call_or_await.md b/docs/docs/en/api/faststream/utils/functions/call_or_await.md deleted file mode 100644 index 9bb63aa18c..0000000000 --- a/docs/docs/en/api/faststream/utils/functions/call_or_await.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: fast_depends.utils.run_async diff --git a/docs/docs/en/api/faststream/utils/functions/drop_response_type.md b/docs/docs/en/api/faststream/utils/functions/drop_response_type.md deleted file mode 100644 index a39e8a2699..0000000000 --- a/docs/docs/en/api/faststream/utils/functions/drop_response_type.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.functions.drop_response_type diff --git a/docs/docs/en/api/faststream/utils/functions/fake_context.md b/docs/docs/en/api/faststream/utils/functions/fake_context.md deleted file mode 100644 index 3943186ba4..0000000000 --- a/docs/docs/en/api/faststream/utils/functions/fake_context.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.functions.fake_context diff --git a/docs/docs/en/api/faststream/utils/functions/return_input.md b/docs/docs/en/api/faststream/utils/functions/return_input.md deleted file mode 100644 index d5514e013f..0000000000 --- a/docs/docs/en/api/faststream/utils/functions/return_input.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.functions.return_input diff --git a/docs/docs/en/api/faststream/utils/functions/sync_fake_context.md b/docs/docs/en/api/faststream/utils/functions/sync_fake_context.md deleted file mode 100644 index 0860846843..0000000000 --- a/docs/docs/en/api/faststream/utils/functions/sync_fake_context.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.functions.sync_fake_context diff --git a/docs/docs/en/api/faststream/utils/functions/timeout_scope.md b/docs/docs/en/api/faststream/utils/functions/timeout_scope.md deleted file mode 100644 index 1577a7593a..0000000000 --- a/docs/docs/en/api/faststream/utils/functions/timeout_scope.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.functions.timeout_scope diff --git a/docs/docs/en/api/faststream/utils/functions/to_async.md b/docs/docs/en/api/faststream/utils/functions/to_async.md deleted file mode 100644 index 715b43d3ac..0000000000 --- a/docs/docs/en/api/faststream/utils/functions/to_async.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.functions.to_async diff --git a/docs/docs/en/api/faststream/utils/no_cast/NoCast.md b/docs/docs/en/api/faststream/utils/no_cast/NoCast.md deleted file mode 100644 index 4fcc6054ba..0000000000 --- a/docs/docs/en/api/faststream/utils/no_cast/NoCast.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.no_cast.NoCast diff --git a/docs/docs/en/api/faststream/utils/nuid/NUID.md b/docs/docs/en/api/faststream/utils/nuid/NUID.md deleted file mode 100644 index 4e43844efe..0000000000 --- a/docs/docs/en/api/faststream/utils/nuid/NUID.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.nuid.NUID diff --git a/docs/docs/en/api/faststream/utils/path/compile_path.md b/docs/docs/en/api/faststream/utils/path/compile_path.md deleted file mode 100644 index 136d5ab1b9..0000000000 --- a/docs/docs/en/api/faststream/utils/path/compile_path.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -# 0.5 - API -# 2 - Release -# 3 - Contributing -# 5 - Template Page -# 10 - Default -search: - boost: 0.5 ---- - -::: faststream.utils.path.compile_path diff --git a/docs/docs/en/faststream.md b/docs/docs/en/faststream.md index aecd7d16ff..aff796cab0 100644 --- a/docs/docs/en/faststream.md +++ b/docs/docs/en/faststream.md @@ -30,11 +30,11 @@ search: - Package version + Package version - Supported Python versions + Supported Python versions
diff --git a/docs/docs/en/getting-started/acknowledgement.md b/docs/docs/en/getting-started/acknowledgement.md new file mode 100644 index 0000000000..ce24027957 --- /dev/null +++ b/docs/docs/en/getting-started/acknowledgement.md @@ -0,0 +1,108 @@ +# Acknowledgment + +Due to the possibility of unexpected errors during message processing, FastStream provides an `ack_policy` parameter that allows users to control how messages are handled. This parameter determines when and how messages should be acknowledged or rejected based on the result of the message processing. + +## AckPolicy + +`AckPolicy` is an enumerated type (`Enum`) in FastStream that specifies the message acknowledgment strategy. It determines how the system responds after receiving and processing a message. + +### Availability + +`AckPolicy` is supported by the following brokers: + +- [**Kafka**](../kafka/index.md){.internal-link} +- [**RabbitMQ**](../rabbit/index.md){.internal-link} +- [**NATS JetStream**](../nats/jetstream/index.md){.internal-link} +- [**Redis Streams**](../redis/streams/index.md){.internal-link} + +### Usage + +You must specify the `ack_policy` parameter when creating a subscriber: + +```python linenums="1" hl_lines="9" +from faststream import FastStream, Logger, AckPolicy +from faststream.nats import NatsBroker + +broker = NatsBroker() +app = FastStream(broker) + +@broker.subscriber( + "test", + ack_policy=AckPolicy.REJECT_ON_ERROR, +) +async def handler(msg: str, logger: Logger) -> None: + logger.info(msg) +``` + +### Available Options + +Each `AckPolicy` variant includes behavior examples for both successful processing and error scenarios. Note that broker-specific behaviors are also included. + +| Policy | Description | On Success | On Error | Broker Notes | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------ | +| `ACK_FIRST` | Acknowledge immediately upon receipt, before processing begins. | Message is acknowledged early;
may be lost if processing fails. | Acknowledged despite error;
message not re-delivered. | Kafka commits offset;
NATS, Redis, and RabbitMQ acknowledge immediately. | +| `ACK` | Acknowledge only after processing completes, regardless of success. | Ack after success. | Ack sent anyway;
message not redelivered. | Kafka: offset commit; others: explicit ack. | +| `REJECT_ON_ERROR` | Reject message if an unhandled exception occurs, permanently discarding it;
otherwise, ack. | Ack after success. | Message discarded; no retry. | RabbitMQ/NATS drops message. Kafka commits offset. | +| `NACK_ON_ERROR` | Nack on error to allow message redelivery, ack after success otherwise. | Ack after success. | Redeliver; attempt to resend message. | Redis Streams and RabbitMQ redelivers; Kafka commits as fallback. | +| `DO_NOTHING` | No automatic acknowledgement. User must manually handle the completion via message methods | | | | + +--- + +### When to Use + +- Use `ACK_FIRST` for scenarios with high throughput where some message loss can be acceptable. +- Use `ACK` if you want the message to be acknowledged, regardless of success or failure. +- Use `REJECT_ON_ERROR` to permanently discard messages on failure. +- Use `NACK_ON_ERROR` to retry messages in case of failure. +- Use `DO_NOTHING` to fully manually control message acknowledgment (for example, calling `#!python message.ack()` yourself). + +--- + +### Extended Examples + +#### Automatic Retry on Failure + +```python linenums="1" hl_lines="7 10" +from faststream import FastStream, AckPolicy, Logger +from faststream.rabbitmq import RabbitBroker + +broker = RabbitBroker() +app = FastStream(broker) + +@broker.subscriber("orders", ack_policy=AckPolicy.NACK_ON_ERROR) +async def process_order(msg: str, logger: Logger) -> None: + logger.info(f"Processing: {msg}") + raise Exception # Message will be nacked and redelivered +``` + +#### Manual Acknowledgment Handling + +```python linenums="1" hl_lines="7 12 14" +from faststream import FastStream, AckPolicy, Logger +from faststream.kafka import KafkaBroker + +broker = KafkaBroker() +app = FastStream(broker) + +@broker.subscriber("events", ack_policy=AckPolicy.DO_NOTHING) +async def handle_event(msg: str) -> None: + try: + # do_smth(msg) + except Exception: + await msg.nack() # or msg.reject() + else: + await msg.ack() +``` + +You can also manage manual acknowledgement using middleware. For more information, [error handling middleware documentation](./middlewares/exception.md){.internal-link}. + +### Broker Behavior Summary + +However, not all brokers support our semantics. Here is a brief overview of **FastStream's** ACK / NACK / REJECT command mapping to brokers' acknowledgment policies: + +| Broker | `ACK` | `NACK` | `REJECT` | +| ------ | ----- | ------ | -------- | +| [RabbitMQ](https://www.rabbitmq.com/docs/confirms#acknowledgement-modes){.external-link target="_blank"} | Protocol ack | Protocol nack | Protocol reject | +| [NATS JetStream](https://docs.nats.io/using-nats/developer/develop_jetstream#acknowledging-messages){.external-link target="_blank"} | Protocol ack | Protocol nak | Protocol term | +| [Redis Streams](https://redis.io/docs/latest/commands/xack/){.external-link target="_blank"} | Xack call | Do nothing | Do nothing | +| Kafka | Commits offset | Do nothing | Do nothing | diff --git a/docs/docs/en/getting-started/asgi.md b/docs/docs/en/getting-started/asgi.md index 516f791351..cbdd804e73 100644 --- a/docs/docs/en/getting-started/asgi.md +++ b/docs/docs/en/getting-started/asgi.md @@ -134,15 +134,18 @@ You can also host your **AsyncAPI** documentation in the same process, by runnin Just create an `AsgiFastStream` object with a special option: -```python linenums="1" hl_lines="8" +```python linenums="1" hl_lines="11" from faststream.nats import NatsBroker from faststream.asgi import AsgiFastStream +from faststream.specification.asyncapi import AsyncAPI +from faststream.asgi import make_asyncapi_asgi broker = NatsBroker() +asyncapi = AsyncAPI(broker) app = AsgiFastStream( broker, - asyncapi_path="/docs", + asgi_routes=("/docs/asyncapi", make_asyncapi_asgi(asyncapi)), ) ``` @@ -152,12 +155,15 @@ Now, your **AsyncAPI HTML** representation can be found by the `/docs` url. You may also use regular `FastStream` application object for similar result. -```python linenums="1" hl_lines="2 11" +```python linenums="1" hl_lines="2 14" from faststream import FastStream from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI +from faststream.asgi import make_asyncapi_asgi, make_ping_asgi, AsgiResponse from faststream.asgi import make_ping_asgi, AsgiResponse broker = NatsBroker() +asyncapi = AsyncAPI(broker) @get async def liveness_ping(scope): @@ -167,8 +173,8 @@ app = FastStream(broker).as_asgi( asgi_routes=[ ("/liveness", liveness_ping), ("/readiness", make_ping_asgi(broker, timeout=5.0)), + ("/docs/asyncapi", make_asyncapi_asgi(asyncapi)) ], - asyncapi_path="/docs", ) ``` @@ -187,15 +193,16 @@ Moreover, our wrappers can be used as ready-to-use endpoints for other **ASGI** Just follow the following example in such cases: -```python linenums="1" hl_lines="6 19-20" +```python linenums="1" hl_lines="6 20-21" from contextlib import asynccontextmanager from fastapi import FastAPI -from faststream import FastStream from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI from faststream.asgi import make_ping_asgi, make_asyncapi_asgi broker = NatsBroker() +asyncapi = AsyncAPI(broker) @asynccontextmanager async def start_broker(app): @@ -207,7 +214,7 @@ async def start_broker(app): app = FastAPI(lifespan=start_broker) app.mount("/health", make_ping_asgi(broker, timeout=5.0)) -app.mount("/asyncapi", make_asyncapi_asgi(FastStream(broker))) +app.mount("/asyncapi", make_asyncapi_asgi(asyncapi)) ``` !!! tip diff --git a/docs/docs/en/getting-started/asyncapi/custom.md b/docs/docs/en/getting-started/asyncapi/custom.md index f5f227d609..612ae41670 100644 --- a/docs/docs/en/getting-started/asyncapi/custom.md +++ b/docs/docs/en/getting-started/asyncapi/custom.md @@ -14,7 +14,7 @@ In this guide, we will explore how to customize **AsyncAPI** documentation for y ## Prerequisites -Before we dive into customization, ensure you have a basic **FastStream** application up and running. If you haven't done that yet, let's setup a simple application right now. +Before we dive into customization, ensure you have a instance **AsyncAPI** specification. If you haven't done that yet, let's create a simple instance right now. Copy the following code in your basic.py file: @@ -124,7 +124,7 @@ To describe your message payload effectively, you can use Pydantic models. Here' Copy the following code in your basic.py file, we have highlighted the creation of payload info and you can see it being passed to the return type and the `msg` argument type in the `on_input_data` function: -```python linenums="1" hl_lines="7-10 19" +```python linenums="1" hl_lines="7-10 24" {! docs_src/getting_started/asyncapi/asyncapi_customization/payload_info.py !} ``` diff --git a/docs/docs/en/getting-started/asyncapi/export.md b/docs/docs/en/getting-started/asyncapi/export.md index f2d23cf9ad..53396af353 100644 --- a/docs/docs/en/getting-started/asyncapi/export.md +++ b/docs/docs/en/getting-started/asyncapi/export.md @@ -24,7 +24,7 @@ Save it in a file called `basic.py`. ## Generating the AsyncAPI Specification -Now that we have a FastStream application, we can proceed with generating the AsyncAPI specification using a CLI command. +Now that we have a FastStream application and specification object, we can proceed with generating the AsyncAPI specification using a CLI command. ```shell {! docs_src/getting_started/asyncapi/serve.py [ln:9] !} diff --git a/docs/docs/en/getting-started/asyncapi/hosting.md b/docs/docs/en/getting-started/asyncapi/hosting.md index 081121cdf9..304b76373d 100644 --- a/docs/docs/en/getting-started/asyncapi/hosting.md +++ b/docs/docs/en/getting-started/asyncapi/hosting.md @@ -21,7 +21,7 @@ search: {! docs_src/getting_started/asyncapi/serve.py [ln:17] !} ``` -In the above command, the path is specified in the format of `python_module:FastStream`. Alternatively, you can also specify `asyncapi.json` or `asyncapi.yaml` to serve the **AsyncAPI** documentation. +In the above command, the path is specified in the format of `python_module:AsyncAPI`. Alternatively, you can also specify `asyncapi.json` or `asyncapi.yaml` to serve the **AsyncAPI** documentation. === "JSON" ```shell @@ -61,15 +61,18 @@ FastStream includes lightweight [**ASGI** support](../asgi.md){.internal-link} t ```python linenums="1" from faststream import FastStream from faststream.kafka import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from faststream.asgi import make_asyncapi_asgi broker = KafkaBroker() +asyncapi = AsyncAPI(broker) @broker.subscriber('topic') async def my_handler(msg: str) -> None: print(msg) app = FastStream(broker).as_asgi( - asyncapi_path="/docs/asyncapi", + asgi_routes=("/docs/asyncapi", make_asyncapi_asgi(asyncapi)), ) if __name__ == "__main__": @@ -85,16 +88,16 @@ After running the script, the **AsyncAPI** docs will be available at: None: @@ -109,23 +112,24 @@ You can choose the method that best fits with your application architecture. app = FastAPI(lifespan=broker_lifespan) @app.get('/docs/asyncapi') - async def asyncapi() -> responses.HTMLResponse: - schema = get_app_schema(FastStream(broker)) - return responses.HTMLResponse(get_asyncapi_html(schema)) + async def docs() -> responses.HTMLResponse: + return responses.HTMLResponse(get_asyncapi_html(asyncapi)) ``` === "Option 2" - ```python linenums="1" hl_lines="23" + ```python linenums="1" hl_lines="25" from typing import AsyncIterator from contextlib import asynccontextmanager from fastapi import FastAPI from faststream import FastStream from faststream.asgi import make_asyncapi_asgi + from faststream.specification.asyncapi import AsyncAPI from faststream.kafka import KafkaBroker broker = KafkaBroker() fs_app = FastStream(broker) + asyncapi = AsyncAPI(broker) @broker.subscriber('topic') async def my_handler(msg: str) -> None: @@ -138,7 +142,7 @@ You can choose the method that best fits with your application architecture. yield app = FastAPI(lifespan=broker_lifespan) - app.mount("/docs/asyncapi", make_asyncapi_asgi(fs_app)) + app.mount("/docs/asyncapi", make_asyncapi_asgi(asyncapi)) ``` After running the app, the documentation will be available at: diff --git a/docs/docs/en/getting-started/cli/index.md b/docs/docs/en/getting-started/cli/index.md index cc90b228a1..8fb3afe0fe 100644 --- a/docs/docs/en/getting-started/cli/index.md +++ b/docs/docs/en/getting-started/cli/index.md @@ -121,30 +121,7 @@ INFO - FastStream app started successfully! To exit press CTRL+C ``` { data-search-exclude } -=== "AIOKafka" - ```python linenums="1" hl_lines="14-16" - {!> docs_src/getting_started/cli/kafka_context.py!} - ``` - -=== "Confluent" - ```python linenums="1" hl_lines="14-16" - {!> docs_src/getting_started/cli/confluent_context.py!} - ``` - -=== "RabbitMQ" - ```python linenums="1" hl_lines="14-16" - {!> docs_src/getting_started/cli/rabbit_context.py [ln:1-10.53,11-] !} - ``` - -=== "NATS" - ```python linenums="1" hl_lines="14-16" - {!> docs_src/getting_started/cli/nats_context.py!} - ``` - -=== "Redis" - ```python linenums="1" hl_lines="14-16" - {!> docs_src/getting_started/cli/redis_context.py!} - ``` +{! includes/en/env-context.md !} !!! note Note that the `env` parameter was passed to the `setup` function directly from the command line diff --git a/docs/docs/en/getting-started/contributing/CONTRIBUTING.md b/docs/docs/en/getting-started/contributing/CONTRIBUTING.md index 745f963830..8c832fda59 100644 --- a/docs/docs/en/getting-started/contributing/CONTRIBUTING.md +++ b/docs/docs/en/getting-started/contributing/CONTRIBUTING.md @@ -12,69 +12,89 @@ search: After cloning the project, you'll need to set up the development environment. Here are the guidelines on how to do this. -## Virtual Environment with `venv` +## Install Justfile Utility -Create a virtual environment in a directory using Python's `venv` module: +Install justfile on your system: ```bash -python -m venv venv +brew install justfile ``` -That will create a `./venv/` directory with Python binaries, allowing you to install packages in an isolated environment. +View all available commands: -## Activate the Environment +```bash +just +``` -Activate the new environment with: +## Init development environment + +Build Python and create a virtual environment: ```bash -source ./venv/bin/activate +just init ``` -Ensure you have the latest pip version in your virtual environment: +By default, this builds Python 3.8. If you need another version, pass it as an argument to the just command: ```bash -python -m pip install --upgrade pip +just init 3.11.5 ``` -## Installing Dependencies +To check available Python versions, refer to the pyproject.toml file in the project root. -After activating the virtual environment as described above, run: +## Activate the Environment + +Activate the new environment with + +For Unix-based systems: ```bash -pip install -e ".[dev]" +source ./venv/bin/activate ``` -This will install all the dependencies and your local **FastStream** in your virtual environment. +For Windows (PowerShell): -### Using Your local **FastStream** +```bash +.\venv\Scripts\Activate.ps1 +``` -If you create a Python file that imports and uses **FastStream**, and run it with the Python from your local environment, it will use your local **FastStream** source code. +Install and configure pre-commit: -Whenever you update your local **FastStream** source code, it will automatically use the latest version when you run your Python file again. This is because it is installed with `-e`. +```bash +just pre-commit-install +``` -This way, you don't have to "install" your local version to be able to test every change. +## Run all Dependencies -To use your local **FastStream CLI**, type: +Start all dependencies as docker containers: ```bash -python -m faststream ... +just up +``` + +Once you are done with development and running tests, you can stop the dependencies' docker containers by running: + +```bash +just stop +# or +just down ``` ## Running Tests -### Pytest +To run tests, use: + +```bash +just test +``` -To run tests with your current **FastStream** application and Python environment, use: +To run tests with coverage: ```bash -pytest tests -# or -./scripts/test.sh -# with coverage output -./scripts/test-cov.sh +just coverage-test ``` -In your project, you'll find some *pytest marks*: +In your project, some tests are grouped under specific pytest marks: * **slow** * **rabbit** @@ -83,34 +103,40 @@ In your project, you'll find some *pytest marks*: * **redis** * **all** -By default, running *pytest* will execute "not slow" tests. - -To run all tests use: +By default, will execute "all" tests. You can specify marks to include or exclude tests: ```bash -pytest -m 'all' +just test kafka +# or +just test rabbit +# or +just test 'not confluent' +# or +just test 'not confluent and not nats' +# or +just coverage-test kafka ``` -If you don't have a local broker instance running, you can run tests without those dependencies: +## Linter + +Run all linters: ```bash -pytest -m 'not rabbit and not kafka and not nats and not redis and not confluent' +just linter ``` -To run tests based on RabbitMQ, Kafka, or other dependencies, the following dependencies are needed to be started as docker containers: +## Static analysis -```yaml -{! includes/docker-compose.yaml !} -``` - -You can start the dependencies easily using provided script by running: +Run static analysis tools: ```bash -./scripts/start_test_env.sh +just static-analysis ``` -Once you are done with development and running tests, you can stop the dependencies' docker containers by running: +## Pre-commit + +Run pre-commit checks: ```bash -./scripts/stop_test_env.sh +just pre-commit ``` diff --git a/docs/docs/en/getting-started/contributing/docs.md b/docs/docs/en/getting-started/contributing/docs.md index 781d797f80..607f6fc2e5 100644 --- a/docs/docs/en/getting-started/contributing/docs.md +++ b/docs/docs/en/getting-started/contributing/docs.md @@ -42,7 +42,7 @@ Enough: ``` 4. Install documentation dependencies ```bash - pip install ".[devdocs]" + pip install --group devdocs -e . ``` 5. Go to the `docs/` directory 6. Start the local documentation server diff --git a/docs/docs/en/getting-started/dependencies/index.md b/docs/docs/en/getting-started/dependencies/index.md index 1759b8c97e..d4f0595ab7 100644 --- a/docs/docs/en/getting-started/dependencies/index.md +++ b/docs/docs/en/getting-started/dependencies/index.md @@ -48,7 +48,7 @@ By default, it applies to all event handlers, unless you disabled the same optio !!! warning Setting the `apply_types=False` flag not only disables type casting but also `Depends` and `Context`. - If you want to disable only type casting, use `validate=False` instead. + If you want to disable only type casting, use `serializer=None` instead. This flag can be useful if you are using **FastStream** within another framework and you need to use its native dependency system. diff --git a/docs/docs/en/getting-started/lifespan/hooks.md b/docs/docs/en/getting-started/lifespan/hooks.md index f234db863b..7784428ea0 100644 --- a/docs/docs/en/getting-started/lifespan/hooks.md +++ b/docs/docs/en/getting-started/lifespan/hooks.md @@ -27,30 +27,7 @@ By [passing optional arguments with the command line](../config/index.md){.inter Let's write some code for our example -=== "AIOKafka" - ```python linenums="1" hl_lines="14-18" - {!> docs_src/getting_started/lifespan/kafka/basic.py!} - ``` - -=== "Confluent" - ```python linenums="1" hl_lines="14-18" - {!> docs_src/getting_started/lifespan/confluent/basic.py!} - ``` - -=== "RabbitMQ" - ```python linenums="1" hl_lines="14-18" - {!> docs_src/getting_started/lifespan/rabbit/basic.py [ln:1-11.53,12-] !} - ``` - -=== "NATS" - ```python linenums="1" hl_lines="14-18" - {!> docs_src/getting_started/lifespan/nats/basic.py!} - ``` - -=== "Redis" - ```python linenums="1" hl_lines="14-18" - {!> docs_src/getting_started/lifespan/redis/basic.py!} - ``` +{! includes/en/env-context.md !} Now this application can be run using the following command to manage the environment: @@ -64,16 +41,16 @@ Now let's look into a little more detail. To begin with, we are using a `#!python @app.on_startup` decorator -```python linenums="14" hl_lines="1" -{! docs_src/getting_started/lifespan/kafka/basic.py [ln:14-18]!} +```python linenums="12" hl_lines="14-15" hl_lines="1" +{! docs_src/getting_started/cli/kafka_context.py [ln:12-15]!} ``` to declare a function that runs when our application starts. The next step is to declare our function parameters that we expect to receive: -```python linenums="14" hl_lines="2" -{! docs_src/getting_started/lifespan/kafka/basic.py [ln:14-18]!} +```python linenums="12" hl_lines="14-15" hl_lines="2" +{! docs_src/getting_started/cli/kafka_context.py [ln:12-15]!} ``` The `env` argument will be passed to the `setup` function from the user-provided command line arguments. @@ -84,8 +61,8 @@ The `env` argument will be passed to the `setup` function from the user-provided Then, we initialize the settings of our application using the file passed to us from the command line: -```python linenums="14" hl_lines="3" -{! docs_src/getting_started/lifespan/kafka/basic.py [ln:14-18] !} +```python linenums="12" hl_lines="14-15" hl_lines="3" +{! docs_src/getting_started/cli/kafka_context.py [ln:12-15]!} ``` And put these settings in a global context: diff --git a/docs/docs/en/getting-started/observability/logging.md b/docs/docs/en/getting-started/observability/logging.md index 4824ef3d28..379b0899a0 100644 --- a/docs/docs/en/getting-started/observability/logging.md +++ b/docs/docs/en/getting-started/observability/logging.md @@ -280,9 +280,9 @@ app = FastStream(broker, logger=logger) And the job is done! Now you have a perfectly structured logs using **Structlog**. ```{.shell .no-copy} -TIMESPAMP [info ] FastStream app starting... extra={} -TIMESPAMP [debug ] `Handler` waiting for messages extra={'topic': 'topic', 'group_id': 'group', 'message_id': ''} -TIMESPAMP [debug ] `Handler` waiting for messages extra={'topic': 'topic', 'group_id': 'group2', 'message_id': ''} -TIMESPAMP [info ] FastStream app started successfully! To exit, press CTRL+C extra={'topic': '', 'group_id': '', 'message_id': ''} +TIMESTAMP [info ] FastStream app starting... extra={} +TIMESTAMP [debug ] `Handler` waiting for messages extra={'topic': 'topic', 'group_id': 'group', 'message_id': ''} +TIMESTAMP [debug ] `Handler` waiting for messages extra={'topic': 'topic', 'group_id': 'group2', 'message_id': ''} +TIMESTAMP [info ] FastStream app started successfully! To exit, press CTRL+C extra={'topic': '', 'group_id': '', 'message_id': ''} ``` { data-search-exclude } diff --git a/docs/docs/en/getting-started/subscription/index.md b/docs/docs/en/getting-started/subscription/index.md index 748e005afc..46405bb85e 100644 --- a/docs/docs/en/getting-started/subscription/index.md +++ b/docs/docs/en/getting-started/subscription/index.md @@ -210,7 +210,7 @@ This way **FastStream** still consumes `#!python json.loads` result, but without !!! warning Setting the `apply_types=False` flag not only disables type casting but also `Depends` and `Context`. - If you want to disable only type casting, use `validate=False` instead. + If you want to disable only type casting, use `serializer=None` instead. ## Multiple Subscriptions diff --git a/docs/docs/en/getting-started/template/index.md b/docs/docs/en/getting-started/template/index.md index 43379390b1..d0b3d0047e 100644 --- a/docs/docs/en/getting-started/template/index.md +++ b/docs/docs/en/getting-started/template/index.md @@ -55,7 +55,7 @@ To set up your development environment, follow these steps: 4. Install all development requirements using pip: ```bash - pip install -e ".[dev]" + pip install --group dev -e . ``` 5. Create a new repository for our FastStream app on GitHub.![creating-new-github-repo](https://github.com/ag2ai/faststream/assets/7011056/7076b925-2090-4bbb-b9da-0df4783fb5a3) diff --git a/docs/docs/en/nats/jetstream/ack.md b/docs/docs/en/nats/jetstream/ack.md index f003966493..a1dbc41168 100644 --- a/docs/docs/en/nats/jetstream/ack.md +++ b/docs/docs/en/nats/jetstream/ack.md @@ -16,29 +16,6 @@ In most cases, **FastStream** automatically acknowledges (*acks*) messages on yo However, there are situations where you might want to use different acknowledgement logic. -## Retries - -If you prefer to use a *nack* instead of a *reject* when there's an error in message processing, you can specify the `retry` flag in the `#!python @broker.subscriber(...)` method, which is responsible for error handling logic. - -By default, this flag is set to `False`, indicating that if an error occurs during message processing, the message can still be retrieved from the queue: - -```python -@broker.subscriber("test", retry=False) # don't handle exceptions -async def base_handler(body: str): - ... -``` - -If this flag is set to `True`, the message will be *nack*ed and placed back in the queue each time an error occurs. In this scenario, the message can be processed by another consumer (if there are several of them) or by the same one: - -```python -@broker.subscriber("test", retry=True) # try again indefinitely -async def base_handler(body: str): - ... -``` - -!!! tip - For more complex error handling cases, you can use [tenacity](https://tenacity.readthedocs.io/en/latest/){.external-link target="_blank"} - ## Manual Acknowledgement If you want to acknowledge a message manually, you can get access directly to the message object via the [Context](../../getting-started/context/existed.md){.internal-link} and call the method. diff --git a/docs/docs/en/rabbit/ack.md b/docs/docs/en/rabbit/ack.md index d68b66a0cd..e7632742dc 100644 --- a/docs/docs/en/rabbit/ack.md +++ b/docs/docs/en/rabbit/ack.md @@ -16,44 +16,6 @@ In most cases, **FastStream** automatically acknowledges (*acks*) messages on yo However, there are situations where you might want to use a different acknowledgement logic. -## Retries - -If you prefer to use a *nack* instead of a *reject* when there's an error in message processing, you can specify the `retry` flag in the `#!python @broker.subscriber(...)` method, which is responsible for error handling logic. - -By default, this flag is set to `False`, indicating that if an error occurs during message processing, the message can still be retrieved from the queue: - -```python -@broker.subscriber("test", retry=False) # don't handle exceptions -async def base_handler(body: str): - ... -``` - -If this flag is set to `True`, the message will be *nack*ed and placed back in the queue each time an error occurs. In this scenario, the message can be processed by another consumer (if there are several of them) or by the same one: - -```python -@broker.subscriber("test", retry=True) # try again indefinitely -async def base_handler(body: str): - ... -``` - -If the `retry` flag is set to an `int`, the message will be placed back in the queue, and the number of retries will be limited to this number: - -```python -@broker.subscriber("test", retry=3) # make up to 3 attempts -async def base_handler(body: str): - ... -``` - -!!! tip - **FastStream** identifies the message by its `message_id`. To make this option work, you should manually set this field on the producer side (if your library doesn't set it automatically). - -!!! bug - At the moment, attempts are counted only by the current consumer. If the message goes to another consumer, it will have its own counter. - Subsequently, this logic will be reworked. - -!!! tip - For more complex error handling cases, you can use [tenacity](https://tenacity.readthedocs.io/en/latest/){.external-link target="_blank"} - ## Manual acknowledgement If you want to acknowledge a message manually, you can get access directly to the message object via the [Context](../getting-started/context/existed.md){.internal-link} and call the method. diff --git a/docs/docs/en/release.md b/docs/docs/en/release.md index 4160d61734..09cf72f054 100644 --- a/docs/docs/en/release.md +++ b/docs/docs/en/release.md @@ -287,7 +287,7 @@ Also, thanks to [@Sehat1137](https://github.com/Sehat1137){.external-link target Well, you (community) made a new breathtaken release for us! Thanks to all of this release contributors. -Special thanks to [@Flosckow](https://github.com/Flosckow){.external-link target="_blank"}. He promores a new perfect feature - concurrent Kafka subscriber (with autocommit mode) +Special thanks to [@Flosckow](https://github.com/Flosckow){.external-link target="_blank"} . He promotes a new perfect feature - concurrent Kafka subscriber (with autocommit mode) ```python from faststream.kafka import KafkaBroker diff --git a/docs/docs/navigation_template.txt b/docs/docs/navigation_template.txt index 19c21b820e..953c8a5019 100644 --- a/docs/docs/navigation_template.txt +++ b/docs/docs/navigation_template.txt @@ -42,6 +42,7 @@ search: - [FastAPI Plugin](getting-started/integrations/fastapi/index.md) - [Django](getting-started/integrations/django/index.md) - [CLI](getting-started/cli/index.md) + - [Acknowledgement](getting-started/acknowledgement.md) - [ASGI](getting-started/asgi.md) - Observability - [Healthcheks](getting-started/observability/healthcheks.md) diff --git a/docs/docs_src/confluent/ack/errors.py b/docs/docs_src/confluent/ack/errors.py index 36ceb61424..72bc9e1aba 100644 --- a/docs/docs_src/confluent/ack/errors.py +++ b/docs/docs_src/confluent/ack/errors.py @@ -1,4 +1,4 @@ -from faststream import FastStream +from faststream import FastStream, AckPolicy from faststream.exceptions import AckMessage from faststream.confluent import KafkaBroker @@ -7,7 +7,7 @@ @broker.subscriber( - "test-error-topic", group_id="test-error-group", auto_commit=False, auto_offset_reset="earliest" + "test-error-topic", group_id="test-error-group", ack_policy=AckPolicy.REJECT_ON_ERROR, auto_offset_reset="earliest" ) async def handle(body): smth_processing(body) diff --git a/docs/docs_src/confluent/batch_consuming_pydantic/app.py b/docs/docs_src/confluent/batch_consuming_pydantic/app.py index 7b95b2c6b9..b1efc43650 100644 --- a/docs/docs_src/confluent/batch_consuming_pydantic/app.py +++ b/docs/docs_src/confluent/batch_consuming_pydantic/app.py @@ -18,5 +18,5 @@ class HelloWorld(BaseModel): @broker.subscriber("test_batch", batch=True) -async def handle_batch(msg: List[HelloWorld], logger: Logger): +async def handle_batch(msg: list[HelloWorld], logger: Logger): logger.info(msg) diff --git a/docs/docs_src/confluent/publisher_object/example.py b/docs/docs_src/confluent/publisher_object/example.py index 9d0deb7970..55fbb64b51 100644 --- a/docs/docs_src/confluent/publisher_object/example.py +++ b/docs/docs_src/confluent/publisher_object/example.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, Field, NonNegativeFloat from faststream import FastStream, Logger -from faststream._compat import model_to_json +from faststream._internal._compat import model_to_json from faststream.confluent import KafkaBroker, TestKafkaBroker broker = KafkaBroker("localhost:9092") diff --git a/docs/docs_src/getting_started/asyncapi/asyncapi_customization/basic.py b/docs/docs_src/getting_started/asyncapi/asyncapi_customization/basic.py index 52c427af6c..0448507d02 100644 --- a/docs/docs_src/getting_started/asyncapi/asyncapi_customization/basic.py +++ b/docs/docs_src/getting_started/asyncapi/asyncapi_customization/basic.py @@ -1,8 +1,10 @@ from faststream import FastStream -from faststream.kafka import KafkaBroker, KafkaMessage +from faststream.kafka import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI broker = KafkaBroker("localhost:9092") app = FastStream(broker) +asyncapi = AsyncAPI(broker) @broker.publisher("output_data") @broker.subscriber("input_data") diff --git a/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_broker.py b/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_broker.py index ac3a8f4234..885a691cee 100644 --- a/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_broker.py +++ b/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_broker.py @@ -1,13 +1,17 @@ from faststream import FastStream -from faststream.kafka import KafkaBroker, KafkaMessage -from faststream.asyncapi.schema import Tag +from faststream.kafka import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI broker = KafkaBroker( "localhost:9092", description="Kafka broker running locally", - asyncapi_url="non-sensitive-url:9092", + specification_url="non-sensitive-url:9092", ) app = FastStream(broker) +asyncapi = AsyncAPI( + broker, + schema_version="2.6.0", +) @broker.publisher("output_data") diff --git a/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_handler.py b/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_handler.py index b48022e133..22da587f37 100644 --- a/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_handler.py +++ b/docs/docs_src/getting_started/asyncapi/asyncapi_customization/custom_handler.py @@ -2,6 +2,7 @@ from faststream import FastStream from faststream.kafka import KafkaBroker, KafkaMessage +from faststream.specification.asyncapi import AsyncAPI class DataBasic(BaseModel): @@ -12,6 +13,10 @@ class DataBasic(BaseModel): broker = KafkaBroker("localhost:9092") app = FastStream(broker) +asyncapi = AsyncAPI( + broker, + schema_version="2.6.0", +) @broker.publisher( 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 7c284c8299..0f9001c4f1 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,18 +1,21 @@ from faststream import FastStream -from faststream.kafka import KafkaBroker, KafkaMessage -from faststream.asyncapi.schema import Contact, ExternalDocs, License, Tag +from faststream.specification.asyncapi import AsyncAPI +from faststream.specification import License, Contact +from faststream.kafka import KafkaBroker broker = KafkaBroker("localhost:9092") description="""# Title of the description This description supports **Markdown** syntax""" -app = FastStream( +app = FastStream(broker) +asyncapi = AsyncAPI( broker, title="My App", - version="1.0.0", + app_version="1.0.0", description=description, license=License(name="MIT", url="https://opensource.org/license/mit/"), terms_of_service="https://my-terms.com/", contact=Contact(name="support", url="https://help.com/"), + schema_version="2.6.0", ) @broker.publisher("output_data") diff --git a/docs/docs_src/getting_started/asyncapi/asyncapi_customization/payload_info.py b/docs/docs_src/getting_started/asyncapi/asyncapi_customization/payload_info.py index 87635921c8..12641807c4 100644 --- a/docs/docs_src/getting_started/asyncapi/asyncapi_customization/payload_info.py +++ b/docs/docs_src/getting_started/asyncapi/asyncapi_customization/payload_info.py @@ -2,6 +2,7 @@ from faststream import FastStream from faststream.kafka import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI class DataBasic(BaseModel): @@ -12,6 +13,10 @@ class DataBasic(BaseModel): broker = KafkaBroker("localhost:9092") app = FastStream(broker) +asyncapi = AsyncAPI( + broker, + schema_version="2.6.0", +) @broker.publisher("output_data") diff --git a/docs/docs_src/getting_started/asyncapi/serve.py b/docs/docs_src/getting_started/asyncapi/serve.py index 5ea752fc2c..0f4c84be8c 100644 --- a/docs/docs_src/getting_started/asyncapi/serve.py +++ b/docs/docs_src/getting_started/asyncapi/serve.py @@ -5,22 +5,22 @@ """ -gen_json_cmd = """ -faststream docs gen basic:app +gen_asyncapi_json_cmd = """ +faststream docs gen basic:asyncapi """ -gen_yaml_cmd = """ -faststream docs gen --yaml basic:app +gen_asyncapi_yaml_cmd = """ +faststream docs gen --yaml basic:asyncapi """ -serve_cmd = """ -faststream docs serve basic:app +asyncapi_serve_cmd = """ +faststream docs serve basic:asyncapi """ -serve_json_cmd = """ +asyncapi_serve_json_cmd = """ faststream docs serve asyncapi.json """ -serve_yaml_cmd = """ +asyncapi_serve_yaml_cmd = """ faststream docs serve asyncapi.yaml """ diff --git a/docs/docs_src/getting_started/cli/confluent_context.py b/docs/docs_src/getting_started/cli/confluent_context.py index e56051963f..bf8c9624f4 100644 --- a/docs/docs_src/getting_started/cli/confluent_context.py +++ b/docs/docs_src/getting_started/cli/confluent_context.py @@ -7,10 +7,9 @@ app = FastStream(broker) class Settings(BaseSettings): - host: str = "localhost:9092" + any_flag: bool @app.on_startup -async def setup(env: str, context: ContextRepo): +async def setup(context: ContextRepo, env: str = ".env"): settings = Settings(_env_file=env) - await broker.connect(settings.host) context.set_global("settings", settings) diff --git a/docs/docs_src/getting_started/cli/kafka_context.py b/docs/docs_src/getting_started/cli/kafka_context.py index b06b9451bc..700523110f 100644 --- a/docs/docs_src/getting_started/cli/kafka_context.py +++ b/docs/docs_src/getting_started/cli/kafka_context.py @@ -7,10 +7,9 @@ app = FastStream(broker) class Settings(BaseSettings): - host: str = "localhost:9092" + any_flag: bool @app.on_startup -async def setup(env: str, context: ContextRepo): +async def setup(context: ContextRepo, env: str = ".env"): settings = Settings(_env_file=env) - await broker.connect(settings.host) context.set_global("settings", settings) diff --git a/docs/docs_src/getting_started/cli/nats_context.py b/docs/docs_src/getting_started/cli/nats_context.py index 4dec4166c3..0ac13cff65 100644 --- a/docs/docs_src/getting_started/cli/nats_context.py +++ b/docs/docs_src/getting_started/cli/nats_context.py @@ -7,10 +7,9 @@ app = FastStream(broker) class Settings(BaseSettings): - host: str = "nats://localhost:4222" + any_flag: bool @app.on_startup -async def setup(env: str, context: ContextRepo): +async def setup(context: ContextRepo, env: str = ".env"): settings = Settings(_env_file=env) - await broker.connect(settings.host) context.set_global("settings", settings) diff --git a/docs/docs_src/getting_started/cli/rabbit_context.py b/docs/docs_src/getting_started/cli/rabbit_context.py index 8d09317131..5f4462da1f 100644 --- a/docs/docs_src/getting_started/cli/rabbit_context.py +++ b/docs/docs_src/getting_started/cli/rabbit_context.py @@ -7,10 +7,9 @@ app = FastStream(broker) class Settings(BaseSettings): - host: str = "amqp://guest:guest@localhost:5672/" # pragma: allowlist secret + any_flag: bool @app.on_startup -async def setup(env: str, context: ContextRepo): +async def setup(context: ContextRepo, env: str = ".env"): settings = Settings(_env_file=env) - await broker.connect(settings.host) context.set_global("settings", settings) diff --git a/docs/docs_src/getting_started/cli/redis_context.py b/docs/docs_src/getting_started/cli/redis_context.py index 79e5967247..4dc97c5d24 100644 --- a/docs/docs_src/getting_started/cli/redis_context.py +++ b/docs/docs_src/getting_started/cli/redis_context.py @@ -7,10 +7,9 @@ app = FastStream(broker) class Settings(BaseSettings): - host: str = "redis://localhost:6379" + any_flag: bool @app.on_startup -async def setup(env: str, context: ContextRepo): +async def setup(context: ContextRepo, env: str = ".env"): settings = Settings(_env_file=env) - await broker.connect(settings.host) context.set_global("settings", settings) diff --git a/docs/docs_src/getting_started/context/confluent/cast.py b/docs/docs_src/getting_started/context/confluent/cast.py index 3d0b14c343..77000f7b5b 100644 --- a/docs/docs_src/getting_started/context/confluent/cast.py +++ b/docs/docs_src/getting_started/context/confluent/cast.py @@ -1,9 +1,9 @@ -from faststream import Context, FastStream, context +from faststream import Context, FastStream from faststream.confluent import KafkaBroker broker = KafkaBroker("localhost:9092") app = FastStream(broker) -context.set_global("secret", "1") +app.context.set_global("secret", "1") @broker.subscriber("test-topic") async def handle( diff --git a/docs/docs_src/getting_started/context/confluent/custom_local_context.py b/docs/docs_src/getting_started/context/confluent/custom_local_context.py index 154c1f87dc..609681fafc 100644 --- a/docs/docs_src/getting_started/context/confluent/custom_local_context.py +++ b/docs/docs_src/getting_started/context/confluent/custom_local_context.py @@ -16,7 +16,7 @@ async def handle( call() -@apply_types +@apply_types(context__=app.context) def call( message: KafkaMessage, correlation_id: str = Context(), diff --git a/docs/docs_src/getting_started/context/confluent/manual_local_context.py b/docs/docs_src/getting_started/context/confluent/manual_local_context.py index 4e2e03531c..4ae8d5ad47 100644 --- a/docs/docs_src/getting_started/context/confluent/manual_local_context.py +++ b/docs/docs_src/getting_started/context/confluent/manual_local_context.py @@ -1,4 +1,4 @@ -from faststream import Context, FastStream, apply_types, context +from faststream import Context, FastStream, apply_types, ContextRepo from faststream.confluent import KafkaBroker from faststream.confluent.annotations import KafkaMessage @@ -10,16 +10,17 @@ async def handle( msg: str, message: KafkaMessage, + context: ContextRepo, ): tag = context.set_local("correlation_id", message.correlation_id) call(tag) -@apply_types +@apply_types(context__=app.context) def call( tag, message: KafkaMessage, correlation_id: str = Context(), ): assert correlation_id == message.correlation_id - context.reset_local("correlation_id", tag) + app.context.reset_local("correlation_id", tag) diff --git a/docs/docs_src/getting_started/context/kafka/cast.py b/docs/docs_src/getting_started/context/kafka/cast.py index 1ef06d3595..00db482531 100644 --- a/docs/docs_src/getting_started/context/kafka/cast.py +++ b/docs/docs_src/getting_started/context/kafka/cast.py @@ -1,9 +1,9 @@ -from faststream import Context, FastStream, context +from faststream import Context, FastStream from faststream.kafka import KafkaBroker broker = KafkaBroker("localhost:9092") app = FastStream(broker) -context.set_global("secret", "1") +app.context.set_global("secret", "1") @broker.subscriber("test-topic") async def handle( diff --git a/docs/docs_src/getting_started/context/kafka/custom_local_context.py b/docs/docs_src/getting_started/context/kafka/custom_local_context.py index 1857c0a7a2..0a7b25edf9 100644 --- a/docs/docs_src/getting_started/context/kafka/custom_local_context.py +++ b/docs/docs_src/getting_started/context/kafka/custom_local_context.py @@ -16,7 +16,7 @@ async def handle( call() -@apply_types +@apply_types(context__=app.context) def call( message: KafkaMessage, correlation_id: str = Context(), diff --git a/docs/docs_src/getting_started/context/kafka/manual_local_context.py b/docs/docs_src/getting_started/context/kafka/manual_local_context.py index 465c41fcb9..9380a487c1 100644 --- a/docs/docs_src/getting_started/context/kafka/manual_local_context.py +++ b/docs/docs_src/getting_started/context/kafka/manual_local_context.py @@ -1,4 +1,4 @@ -from faststream import Context, FastStream, apply_types, context +from faststream import Context, FastStream, apply_types, ContextRepo from faststream.kafka import KafkaBroker from faststream.kafka.annotations import KafkaMessage @@ -10,16 +10,17 @@ async def handle( msg: str, message: KafkaMessage, + context: ContextRepo, ): tag = context.set_local("correlation_id", message.correlation_id) call(tag) -@apply_types +@apply_types(context__=app.context) def call( tag, message: KafkaMessage, correlation_id: str = Context(), ): assert correlation_id == message.correlation_id - context.reset_local("correlation_id", tag) + app.context.reset_local("correlation_id", tag) diff --git a/docs/docs_src/getting_started/context/nats/cast.py b/docs/docs_src/getting_started/context/nats/cast.py index 0733561043..128cb19dd8 100644 --- a/docs/docs_src/getting_started/context/nats/cast.py +++ b/docs/docs_src/getting_started/context/nats/cast.py @@ -1,9 +1,9 @@ -from faststream import Context, FastStream, context +from faststream import Context, FastStream from faststream.nats import NatsBroker broker = NatsBroker("nats://localhost:4222") app = FastStream(broker) -context.set_global("secret", "1") +app.context.set_global("secret", "1") @broker.subscriber("test-subject") async def handle( diff --git a/docs/docs_src/getting_started/context/nats/custom_local_context.py b/docs/docs_src/getting_started/context/nats/custom_local_context.py index 5a158fdd0a..f6edd037b5 100644 --- a/docs/docs_src/getting_started/context/nats/custom_local_context.py +++ b/docs/docs_src/getting_started/context/nats/custom_local_context.py @@ -16,7 +16,7 @@ async def handle( call() -@apply_types +@apply_types(context__=app.context) def call( message: NatsMessage, correlation_id: str = Context(), diff --git a/docs/docs_src/getting_started/context/nats/manual_local_context.py b/docs/docs_src/getting_started/context/nats/manual_local_context.py index 90dffb55a3..8cbb7337a9 100644 --- a/docs/docs_src/getting_started/context/nats/manual_local_context.py +++ b/docs/docs_src/getting_started/context/nats/manual_local_context.py @@ -1,4 +1,4 @@ -from faststream import Context, FastStream, apply_types, context +from faststream import Context, FastStream, apply_types from faststream.nats import NatsBroker from faststream.nats.annotations import NatsMessage @@ -11,15 +11,15 @@ async def handle( msg: str, message: NatsMessage, ): - tag = context.set_local("correlation_id", message.correlation_id) + tag = app.context.set_local("correlation_id", message.correlation_id) call(tag) -@apply_types +@apply_types(context__=app.context) def call( tag, message: NatsMessage, correlation_id: str = Context(), ): assert correlation_id == message.correlation_id - context.reset_local("correlation_id", tag) + app.context.reset_local("correlation_id", tag) diff --git a/docs/docs_src/getting_started/context/nested.py b/docs/docs_src/getting_started/context/nested.py index 645c267695..533360628c 100644 --- a/docs/docs_src/getting_started/context/nested.py +++ b/docs/docs_src/getting_started/context/nested.py @@ -12,6 +12,6 @@ async def handler(body): nested_func(body) -@apply_types +@apply_types(context__=broker.context) def nested_func(body, logger: Logger = Context()): logger.info(body) diff --git a/docs/docs_src/getting_started/context/rabbit/cast.py b/docs/docs_src/getting_started/context/rabbit/cast.py index 24cf1bf72e..47ce2b4525 100644 --- a/docs/docs_src/getting_started/context/rabbit/cast.py +++ b/docs/docs_src/getting_started/context/rabbit/cast.py @@ -1,9 +1,9 @@ -from faststream import Context, FastStream, context +from faststream import Context, FastStream from faststream.rabbit import RabbitBroker broker = RabbitBroker("amqp://guest:guest@localhost:5672/") app = FastStream(broker) -context.set_global("secret", "1") +app.context.set_global("secret", "1") @broker.subscriber("test-queue") async def handle( diff --git a/docs/docs_src/getting_started/context/rabbit/custom_local_context.py b/docs/docs_src/getting_started/context/rabbit/custom_local_context.py index 76d189b4ed..5e1a8e3bcb 100644 --- a/docs/docs_src/getting_started/context/rabbit/custom_local_context.py +++ b/docs/docs_src/getting_started/context/rabbit/custom_local_context.py @@ -16,7 +16,7 @@ async def handle( call() -@apply_types +@apply_types(context__=app.context) def call( message: RabbitMessage, correlation_id: str = Context(), diff --git a/docs/docs_src/getting_started/context/rabbit/manual_local_context.py b/docs/docs_src/getting_started/context/rabbit/manual_local_context.py index 72d745e4b7..cd3507ad29 100644 --- a/docs/docs_src/getting_started/context/rabbit/manual_local_context.py +++ b/docs/docs_src/getting_started/context/rabbit/manual_local_context.py @@ -1,4 +1,4 @@ -from faststream import Context, FastStream, apply_types, context +from faststream import Context, FastStream, apply_types from faststream.rabbit import RabbitBroker from faststream.rabbit.annotations import RabbitMessage @@ -11,15 +11,15 @@ async def handle( msg: str, message: RabbitMessage, ): - tag = context.set_local("correlation_id", message.correlation_id) + tag = app.context.set_local("correlation_id", message.correlation_id) call(tag) -@apply_types +@apply_types(context__=app.context) def call( tag, message: RabbitMessage, correlation_id: str = Context(), ): assert correlation_id == message.correlation_id - context.reset_local("correlation_id", tag) + app.context.reset_local("correlation_id", tag) diff --git a/docs/docs_src/getting_started/context/redis/cast.py b/docs/docs_src/getting_started/context/redis/cast.py index fbd5eaeb3b..203daafb30 100644 --- a/docs/docs_src/getting_started/context/redis/cast.py +++ b/docs/docs_src/getting_started/context/redis/cast.py @@ -1,9 +1,9 @@ -from faststream import Context, FastStream, context +from faststream import Context, FastStream from faststream.redis import RedisBroker broker = RedisBroker("redis://localhost:6379") app = FastStream(broker) -context.set_global("secret", "1") +app.context.set_global("secret", "1") @broker.subscriber("test-channel") async def handle( diff --git a/docs/docs_src/getting_started/context/redis/custom_local_context.py b/docs/docs_src/getting_started/context/redis/custom_local_context.py index 548c70b57d..5fa89326b8 100644 --- a/docs/docs_src/getting_started/context/redis/custom_local_context.py +++ b/docs/docs_src/getting_started/context/redis/custom_local_context.py @@ -16,7 +16,7 @@ async def handle( call() -@apply_types +@apply_types(context__=app.context) def call( message: RedisMessage, correlation_id: str = Context(), diff --git a/docs/docs_src/getting_started/context/redis/manual_local_context.py b/docs/docs_src/getting_started/context/redis/manual_local_context.py index 302a2d599c..c9b09a41d2 100644 --- a/docs/docs_src/getting_started/context/redis/manual_local_context.py +++ b/docs/docs_src/getting_started/context/redis/manual_local_context.py @@ -1,4 +1,4 @@ -from faststream import Context, FastStream, apply_types, context +from faststream import Context, FastStream, apply_types from faststream.redis import RedisBroker from faststream.redis.annotations import RedisMessage @@ -11,15 +11,15 @@ async def handle( msg: str, message: RedisMessage, ): - tag = context.set_local("correlation_id", message.correlation_id) + tag = app.context.set_local("correlation_id", message.correlation_id) call(tag) -@apply_types +@apply_types(context__=app.context) def call( tag, message: RedisMessage, correlation_id: str = Context(), ): assert correlation_id == message.correlation_id - context.reset_local("correlation_id", tag) + app.context.reset_local("correlation_id", tag) diff --git a/docs/docs_src/getting_started/lifespan/multiple.py b/docs/docs_src/getting_started/lifespan/multiple.py index f0280d4da4..d1d6fd75f6 100644 --- a/docs/docs_src/getting_started/lifespan/multiple.py +++ b/docs/docs_src/getting_started/lifespan/multiple.py @@ -1,6 +1,8 @@ +from unittest.mock import AsyncMock + from faststream import Context, ContextRepo, FastStream -app = FastStream() +app = FastStream(AsyncMock()) @app.on_startup diff --git a/docs/docs_src/getting_started/subscription/confluent/real_testing.py b/docs/docs_src/getting_started/subscription/confluent/real_testing.py index a0a0e4cf3a..8bc4dd10d7 100644 --- a/docs/docs_src/getting_started/subscription/confluent/real_testing.py +++ b/docs/docs_src/getting_started/subscription/confluent/real_testing.py @@ -1,5 +1,5 @@ import pytest -from pydantic import ValidationError +from fast_depends.exceptions import ValidationError from faststream.confluent import TestKafkaBroker diff --git a/docs/docs_src/getting_started/subscription/confluent/testing.py b/docs/docs_src/getting_started/subscription/confluent/testing.py index 57ed6acaaa..dfb2bf964d 100644 --- a/docs/docs_src/getting_started/subscription/confluent/testing.py +++ b/docs/docs_src/getting_started/subscription/confluent/testing.py @@ -1,5 +1,5 @@ import pytest -from pydantic import ValidationError +from fast_depends.exceptions import ValidationError from faststream.confluent import TestKafkaBroker diff --git a/docs/docs_src/getting_started/subscription/kafka/real_testing.py b/docs/docs_src/getting_started/subscription/kafka/real_testing.py index 0cf374b233..5eb6fd7817 100644 --- a/docs/docs_src/getting_started/subscription/kafka/real_testing.py +++ b/docs/docs_src/getting_started/subscription/kafka/real_testing.py @@ -1,5 +1,5 @@ import pytest -from pydantic import ValidationError +from fast_depends.exceptions import ValidationError from faststream.kafka import TestKafkaBroker diff --git a/docs/docs_src/getting_started/subscription/kafka/testing.py b/docs/docs_src/getting_started/subscription/kafka/testing.py index e1f6241276..cf834ff802 100644 --- a/docs/docs_src/getting_started/subscription/kafka/testing.py +++ b/docs/docs_src/getting_started/subscription/kafka/testing.py @@ -1,5 +1,5 @@ import pytest -from pydantic import ValidationError +from fast_depends.exceptions import ValidationError from faststream.kafka import TestKafkaBroker diff --git a/docs/docs_src/getting_started/subscription/nats/real_testing.py b/docs/docs_src/getting_started/subscription/nats/real_testing.py index 5e9d6e4567..c14123218c 100644 --- a/docs/docs_src/getting_started/subscription/nats/real_testing.py +++ b/docs/docs_src/getting_started/subscription/nats/real_testing.py @@ -1,5 +1,5 @@ import pytest -from pydantic import ValidationError +from fast_depends.exceptions import ValidationError from faststream.nats import TestNatsBroker diff --git a/docs/docs_src/getting_started/subscription/nats/testing.py b/docs/docs_src/getting_started/subscription/nats/testing.py index 0f7560e043..4d66a744c0 100644 --- a/docs/docs_src/getting_started/subscription/nats/testing.py +++ b/docs/docs_src/getting_started/subscription/nats/testing.py @@ -1,5 +1,5 @@ import pytest -from pydantic import ValidationError +from fast_depends.exceptions import ValidationError from faststream.nats import TestNatsBroker diff --git a/docs/docs_src/getting_started/subscription/rabbit/real_testing.py b/docs/docs_src/getting_started/subscription/rabbit/real_testing.py index 900b6046e7..7cf61a2df5 100644 --- a/docs/docs_src/getting_started/subscription/rabbit/real_testing.py +++ b/docs/docs_src/getting_started/subscription/rabbit/real_testing.py @@ -1,5 +1,5 @@ import pytest -from pydantic import ValidationError +from fast_depends.exceptions import ValidationError from faststream.rabbit import TestRabbitBroker diff --git a/docs/docs_src/getting_started/subscription/rabbit/testing.py b/docs/docs_src/getting_started/subscription/rabbit/testing.py index 78425924da..f49be05c7a 100644 --- a/docs/docs_src/getting_started/subscription/rabbit/testing.py +++ b/docs/docs_src/getting_started/subscription/rabbit/testing.py @@ -1,5 +1,5 @@ import pytest -from pydantic import ValidationError +from fast_depends.exceptions import ValidationError from faststream.rabbit import TestRabbitBroker diff --git a/docs/docs_src/getting_started/subscription/redis/real_testing.py b/docs/docs_src/getting_started/subscription/redis/real_testing.py index b2c05c203e..6514d66902 100644 --- a/docs/docs_src/getting_started/subscription/redis/real_testing.py +++ b/docs/docs_src/getting_started/subscription/redis/real_testing.py @@ -1,5 +1,5 @@ import pytest -from pydantic import ValidationError +from fast_depends.exceptions import ValidationError from faststream.redis import TestRedisBroker diff --git a/docs/docs_src/getting_started/subscription/redis/testing.py b/docs/docs_src/getting_started/subscription/redis/testing.py index 4934366f75..bb38ffd5fe 100644 --- a/docs/docs_src/getting_started/subscription/redis/testing.py +++ b/docs/docs_src/getting_started/subscription/redis/testing.py @@ -1,5 +1,5 @@ import pytest -from pydantic import ValidationError +from fast_depends.exceptions import ValidationError from faststream.redis import TestRedisBroker diff --git a/docs/docs_src/index/confluent/test.py b/docs/docs_src/index/confluent/test.py index 1cc613d157..b569184a81 100644 --- a/docs/docs_src/index/confluent/test.py +++ b/docs/docs_src/index/confluent/test.py @@ -1,7 +1,7 @@ from .pydantic import broker import pytest -import pydantic +from fast_depends.exceptions import ValidationError from faststream.confluent import TestKafkaBroker @@ -16,5 +16,5 @@ async def test_correct(): @pytest.mark.asyncio async def test_invalid(): async with TestKafkaBroker(broker) as br: - with pytest.raises(pydantic.ValidationError): + with pytest.raises(ValidationError): await br.publish("wrong message", "in-topic") diff --git a/docs/docs_src/index/kafka/test.py b/docs/docs_src/index/kafka/test.py index bfd740312c..67b57e6f12 100644 --- a/docs/docs_src/index/kafka/test.py +++ b/docs/docs_src/index/kafka/test.py @@ -1,7 +1,7 @@ from .pydantic import broker import pytest -import pydantic +from fast_depends.exceptions import ValidationError from faststream.kafka import TestKafkaBroker @@ -16,5 +16,5 @@ async def test_correct(): @pytest.mark.asyncio async def test_invalid(): async with TestKafkaBroker(broker) as br: - with pytest.raises(pydantic.ValidationError): + with pytest.raises(ValidationError): await br.publish("wrong message", "in-topic") diff --git a/docs/docs_src/index/nats/test.py b/docs/docs_src/index/nats/test.py index 85b2e6de76..ca2e71e7b9 100644 --- a/docs/docs_src/index/nats/test.py +++ b/docs/docs_src/index/nats/test.py @@ -1,7 +1,7 @@ from .pydantic import broker import pytest -import pydantic +from fast_depends.exceptions import ValidationError from faststream.nats import TestNatsBroker @@ -16,5 +16,5 @@ async def test_correct(): @pytest.mark.asyncio async def test_invalid(): async with TestNatsBroker(broker) as br: - with pytest.raises(pydantic.ValidationError): + with pytest.raises(ValidationError): await br.publish("wrong message", "in-subject") diff --git a/docs/docs_src/index/rabbit/test.py b/docs/docs_src/index/rabbit/test.py index a193db35b2..7b67df49dc 100644 --- a/docs/docs_src/index/rabbit/test.py +++ b/docs/docs_src/index/rabbit/test.py @@ -1,7 +1,7 @@ from .pydantic import broker import pytest -import pydantic +from fast_depends.exceptions import ValidationError from faststream.rabbit import TestRabbitBroker @@ -16,5 +16,5 @@ async def test_correct(): @pytest.mark.asyncio async def test_invalid(): async with TestRabbitBroker(broker) as br: - with pytest.raises(pydantic.ValidationError): + with pytest.raises(ValidationError): await br.publish("wrong message", "in-queue") diff --git a/docs/docs_src/index/redis/test.py b/docs/docs_src/index/redis/test.py index 9a14ba4190..411e032edb 100644 --- a/docs/docs_src/index/redis/test.py +++ b/docs/docs_src/index/redis/test.py @@ -1,7 +1,7 @@ from .pydantic import broker import pytest -import pydantic +from fast_depends.exceptions import ValidationError from faststream.redis import TestRedisBroker @@ -16,5 +16,5 @@ async def test_correct(): @pytest.mark.asyncio async def test_invalid(): async with TestRedisBroker(broker) as br: - with pytest.raises(pydantic.ValidationError): + with pytest.raises(ValidationError): await br.publish("wrong message", "in-channel") diff --git a/docs/docs_src/kafka/ack/errors.py b/docs/docs_src/kafka/ack/errors.py index 19d333976d..6f293ab681 100644 --- a/docs/docs_src/kafka/ack/errors.py +++ b/docs/docs_src/kafka/ack/errors.py @@ -1,4 +1,4 @@ -from faststream import FastStream +from faststream import FastStream, AckPolicy from faststream.exceptions import AckMessage from faststream.kafka import KafkaBroker @@ -7,7 +7,7 @@ @broker.subscriber( - "test-topic", group_id="test-group", auto_commit=False + "test-topic", group_id="test-group", ack_policy=AckPolicy.REJECT_ON_ERROR, ) async def handle(body): smth_processing(body) diff --git a/docs/docs_src/kafka/basic/basic.py b/docs/docs_src/kafka/basic/basic.py index 58d1666a1b..29b14a63f7 100644 --- a/docs/docs_src/kafka/basic/basic.py +++ b/docs/docs_src/kafka/basic/basic.py @@ -2,6 +2,7 @@ from faststream import FastStream, Logger from faststream.kafka import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI class DataBasic(BaseModel): @@ -12,6 +13,7 @@ class DataBasic(BaseModel): broker = KafkaBroker("localhost:9092") app = FastStream(broker) +asyncapi = AsyncAPI(broker, schema_version="3.0.0") @broker.publisher("output_data") diff --git a/docs/docs_src/kafka/batch_consuming_pydantic/app.py b/docs/docs_src/kafka/batch_consuming_pydantic/app.py index dfc9a6d3ce..60d2022c3a 100644 --- a/docs/docs_src/kafka/batch_consuming_pydantic/app.py +++ b/docs/docs_src/kafka/batch_consuming_pydantic/app.py @@ -18,5 +18,5 @@ class HelloWorld(BaseModel): @broker.subscriber("test_batch", batch=True) -async def handle_batch(msg: List[HelloWorld], logger: Logger): +async def handle_batch(msg: list[HelloWorld], logger: Logger): logger.info(msg) diff --git a/docs/docs_src/kafka/publisher_object/example.py b/docs/docs_src/kafka/publisher_object/example.py index d1aaab90be..f1f4c48367 100644 --- a/docs/docs_src/kafka/publisher_object/example.py +++ b/docs/docs_src/kafka/publisher_object/example.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, Field, NonNegativeFloat from faststream import FastStream, Logger -from faststream._compat import model_to_json +from faststream._internal._compat import model_to_json from faststream.kafka import KafkaBroker, TestKafkaBroker broker = KafkaBroker("localhost:9092") diff --git a/docs/docs_src/nats/js/main.py b/docs/docs_src/nats/js/main.py index a65749b082..715e04fd4b 100644 --- a/docs/docs_src/nats/js/main.py +++ b/docs/docs_src/nats/js/main.py @@ -1,5 +1,6 @@ from faststream import FastStream, Logger from faststream.nats import JStream, NatsBroker +from nats.js.api import DeliverPolicy broker = NatsBroker() app = FastStream(broker) @@ -9,7 +10,7 @@ @broker.subscriber( "js-subject", stream=stream, - deliver_policy="new", + deliver_policy=DeliverPolicy.NEW, ) async def handler(msg: str, logger: Logger): logger.info(msg) diff --git a/docs/docs_src/rabbit/subscription/stream.py b/docs/docs_src/rabbit/subscription/stream.py index 17c99753cf..c9da4707aa 100644 --- a/docs/docs_src/rabbit/subscription/stream.py +++ b/docs/docs_src/rabbit/subscription/stream.py @@ -1,7 +1,7 @@ from faststream import FastStream, Logger -from faststream.rabbit import RabbitBroker, RabbitQueue, QueueType +from faststream.rabbit import RabbitBroker, RabbitQueue, QueueType, Channel -broker = RabbitBroker(max_consumers=10) +broker = RabbitBroker(default_channel=Channel(prefetch_count=10)) app = FastStream(broker) queue = RabbitQueue( @@ -15,10 +15,10 @@ queue, consume_args={"x-stream-offset": "first"}, ) -async def handle(msg, logger: Logger): +async def handle(msg, logger: Logger) -> None: logger.info(msg) @app.after_startup -async def test(): +async def test() -> None: await broker.publish("Hi!", queue) diff --git a/docs/docs_src/redis/list/list_pub.py b/docs/docs_src/redis/list/list_pub.py index 0bb42ef626..082650629a 100644 --- a/docs/docs_src/redis/list/list_pub.py +++ b/docs/docs_src/redis/list/list_pub.py @@ -10,7 +10,7 @@ class Data(BaseModel): ) -broker = RedisBroker("localhost:6379") +broker = RedisBroker("redis://localhost:6379") app = FastStream(broker) diff --git a/docs/expand_markdown.py b/docs/expand_markdown.py new file mode 100644 index 0000000000..1d06c6fb06 --- /dev/null +++ b/docs/expand_markdown.py @@ -0,0 +1,112 @@ +import logging +import re +from pathlib import Path +from typing import Optional + +import typer + +logging.basicConfig(level=logging.INFO) + + +app = typer.Typer() + + +def read_lines_from_file(file_path: Path, lines_spec: Optional[str]) -> str: + """Read lines from a file. + + Args: + file_path: The path to the file. + lines_spec: A comma-separated string of line numbers and/or line ranges. + + Returns: + A string containing the lines from the file. + """ + with file_path.open() as file: + all_lines = file.readlines() + + # Check if lines_spec is empty (indicating all lines should be read) + if not lines_spec: + return "".join(all_lines) + + selected_lines = [] + line_specs = lines_spec.split(",") + + for line_spec in line_specs: + if "-" in line_spec: + # Handle line ranges (e.g., "1-10") + start, end = map(int, line_spec.split("-")) + selected_lines.extend(all_lines[start - 1 : end]) + else: + # Handle single line numbers + line_number = int(line_spec) + if 1 <= line_number <= len(all_lines): + selected_lines.append(all_lines[line_number - 1]) + + return "".join(selected_lines) + + +def extract_lines(embedded_line: str) -> str: + to_expand_path_elements = re.search("{!>(.*)!}", embedded_line).group(1).strip() + lines_spec = "" + if "[ln:" in to_expand_path_elements: + to_expand_path_elements, lines_spec = to_expand_path_elements.split("[ln:") + to_expand_path_elements = to_expand_path_elements.strip() + lines_spec = lines_spec[:-1] + + if Path("./docs/docs_src").exists(): + base_path = Path("./docs") + elif Path("./docs_src").exists(): + base_path = Path("./") + else: + raise ValueError("Couldn't find docs_src directory") + + return read_lines_from_file(base_path / to_expand_path_elements, lines_spec) + + +@app.command() +def expand_markdown( + input_markdown_path: Path = typer.Argument(...), + output_markdown_path: Path = typer.Argument(...), +): + with ( + input_markdown_path.open() as input_file, + output_markdown_path.open( + "w", + ) as output_file, + ): + for line in input_file: + # Check if the line does not contain the "{!>" pattern + if "{!>" not in line: + # Write the line to the output file + output_file.write(line) + else: + output_file.write(extract_lines(embedded_line=line)) + + +def remove_lines_between_dashes(file_path: Path): + with file_path.open() as file: + lines = file.readlines() + + start_dash_index = None + end_dash_index = None + new_lines = [] + + for index, line in enumerate(lines): + if line.strip() == "---": + if start_dash_index is None: + start_dash_index = index + else: + end_dash_index = index + # Remove lines between the two dashes + new_lines = ( + lines[:start_dash_index] + new_lines + lines[end_dash_index + 1 :] + ) + start_dash_index = end_dash_index = None + break # NOTE: Remove this line if you have multiple dash chunks + + with file_path.open("w") as file: + file.writelines(new_lines) + + +if __name__ == "__main__": + app() diff --git a/docs/includes/docker-compose.yaml b/docs/includes/docker-compose.yaml index 4ed4ceef61..045b3d0f7d 100644 --- a/docs/includes/docker-compose.yaml +++ b/docs/includes/docker-compose.yaml @@ -2,9 +2,10 @@ version: "3" services: # nosemgrep: yaml.docker-compose.security.writable-filesystem-service.writable-filesystem-service rabbitmq: - image: rabbitmq:alpine + image: rabbitmq:4-management ports: - "5672:5672" + - "15672:15672" # https://semgrep.dev/r?q=yaml.docker-compose.security.no-new-privileges.no-new-privileges security_opt: - no-new-privileges:true diff --git a/docs/includes/en/env-context.md b/docs/includes/en/env-context.md new file mode 100644 index 0000000000..9f6949dd19 --- /dev/null +++ b/docs/includes/en/env-context.md @@ -0,0 +1,24 @@ +=== "AIOKafka" + ```python linenums="1" hl_lines="14-15" + {!> docs_src/getting_started/cli/kafka_context.py!} + ``` + +=== "Confluent" + ```python linenums="1" hl_lines="14-15" + {!> docs_src/getting_started/cli/confluent_context.py!} + ``` + +=== "RabbitMQ" + ```python linenums="1" hl_lines="14-15" + {!> docs_src/getting_started/cli/rabbit_context.py !} + ``` + +=== "NATS" + ```python linenums="1" hl_lines="14-15" + {!> docs_src/getting_started/cli/nats_context.py!} + ``` + +=== "Redis" + ```python linenums="1" hl_lines="14-15" + {!> docs_src/getting_started/cli/redis_context.py!} + ``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 47b97f174b..cb52355d6a 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -156,9 +156,9 @@ markdown_extensions: alternate_style: true # create tabs group - attr_list # specify html attrs in markdown - md_in_html # render md wrapped to html tags - - pymdownx.emoji: # render material icons - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg + # - pymdownx.emoji: # render material icons + # emoji_index: !!python/name:material.extensions.emoji.twemoji + # emoji_generator: !!python/name:material.extensions.emoji.to_svg extra: analytics: diff --git a/docs/update_releases.py b/docs/update_releases.py index bc80432774..9199326786 100644 --- a/docs/update_releases.py +++ b/docs/update_releases.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import List, Sequence, Tuple -import requests +import httpx def find_metablock(lines: List[str]) -> Tuple[List[str], List[str]]: @@ -27,7 +27,7 @@ def find_header(lines: List[str]) -> Tuple[str, List[str]]: def get_github_releases() -> Sequence[Tuple[str, str]]: # Get the latest version from GitHub releases - response = requests.get("https://api.github.com/repos/ag2ai/FastStream/releases") + response = httpx.get("https://api.github.com/repos/ag2ai/FastStream/releases") return ((x["tag_name"], x["body"]) for x in reversed(response.json())) @@ -35,7 +35,9 @@ def convert_links_and_usernames(text): if "](" not in text: # Convert HTTP/HTTPS links text = re.sub( - r"(https?://.*\/(.*))", r'[#\2](\1){.external-link target="_blank"}', text + r"(https?://.*\/(.*))", + r'[#\2](\1){.external-link target="_blank"}', + text, ) # Convert GitHub usernames to links @@ -83,7 +85,7 @@ def update_release_notes(realease_notes_path: Path): + "\n" # adding an addition newline after the header results in one empty file being added every time we run the script + changelog + "\n" - ).replace("\r", "") + ).replace("\r", ""), ) diff --git a/examples/e04_msg_filter.py b/examples/e04_msg_filter.py index 435076c6db..a09c1d6db6 100644 --- a/examples/e04_msg_filter.py +++ b/examples/e04_msg_filter.py @@ -5,9 +5,9 @@ broker = RabbitBroker("amqp://guest:guest@localhost:5672/") app = FastStream(broker) - subscriber = broker.subscriber("test-queue") + @subscriber(filter=lambda m: m.content_type == "application/json") async def handle_json(msg, logger: Logger): logger.info("JSON message: %s", msg) diff --git a/examples/e10_middlewares.py b/examples/e10_middlewares.py index 03a0519d79..31a2a257c9 100644 --- a/examples/e10_middlewares.py +++ b/examples/e10_middlewares.py @@ -25,7 +25,7 @@ async def subscriber_middleware( msg: RabbitMessage, ) -> Any: print(f"call handler middleware with body: {msg}") - msg._decoded_body = "fake message" + msg.body = b"fake message" result = await call_next(msg) print("handler middleware out") return result diff --git a/examples/kafka/ack_after_process.py b/examples/kafka/ack_after_process.py index 7a00b7fac7..97550fdb87 100644 --- a/examples/kafka/ack_after_process.py +++ b/examples/kafka/ack_after_process.py @@ -1,14 +1,13 @@ -from faststream import FastStream, Logger +from faststream import FastStream, Logger, AckPolicy from faststream.kafka import KafkaBroker broker = KafkaBroker() app = FastStream(broker) - @broker.subscriber( "test", group_id="group", - auto_commit=False, + ack_policy=AckPolicy.REJECT_ON_ERROR, ) async def handler(msg: str, logger: Logger): logger.info(msg) diff --git a/examples/rabbit/stream.py b/examples/rabbit/stream.py index 65cd0e5f30..06d4a8f516 100644 --- a/examples/rabbit/stream.py +++ b/examples/rabbit/stream.py @@ -1,11 +1,11 @@ from faststream import FastStream, Logger -from faststream.rabbit import RabbitBroker, RabbitQueue +from faststream.rabbit import RabbitBroker, RabbitQueue, Channel -broker = RabbitBroker(max_consumers=10) +broker = RabbitBroker(default_channel=Channel(prefetch_count=10)) app = FastStream(broker) queue = RabbitQueue( - name="test", + name="stream-test", durable=True, arguments={ "x-queue-type": "stream", @@ -17,10 +17,10 @@ queue, consume_args={"x-stream-offset": "first"}, ) -async def handle(msg, logger: Logger): +async def handle(msg, logger: Logger) -> None: logger.info(msg) @app.after_startup -async def test(): +async def test() -> None: await broker.publish("Hi!", queue) diff --git a/faststream/__init__.py b/faststream/__init__.py index b4267f7895..cad7e628bf 100644 --- a/faststream/__init__.py +++ b/faststream/__init__.py @@ -1,15 +1,24 @@ """A Python framework for building services interacting with Apache Kafka, RabbitMQ, NATS and Redis.""" -from faststream.annotations import ContextRepo, Logger, NoCast +from faststream._internal.testing.app import TestApp +from faststream._internal.utils import apply_types +from faststream.annotations import ContextRepo, Logger from faststream.app import FastStream -from faststream.broker.middlewares import BaseMiddleware, ExceptionMiddleware -from faststream.broker.response import Response -from faststream.testing.app import TestApp -from faststream.utils import Context, Depends, Header, Path, apply_types, context +from faststream.middlewares import AckPolicy, BaseMiddleware, ExceptionMiddleware +from faststream.params import ( + Context, + Depends, + Header, + NoCast, + Path, +) +from faststream.response import Response __all__ = ( # middlewares + "AckPolicy", "BaseMiddleware", + # params "Context", "ContextRepo", "Depends", @@ -26,5 +35,4 @@ "TestApp", # utils "apply_types", - "context", ) diff --git a/faststream/__main__.py b/faststream/__main__.py index ba92a618a6..af09631a11 100644 --- a/faststream/__main__.py +++ b/faststream/__main__.py @@ -1,23 +1,21 @@ """CLI entry point to FastStream framework.""" -import warnings - -try: - from faststream.cli.main import cli -except ImportError: - has_typer = False -else: - has_typer = True +from faststream._internal._compat import HAS_TYPER -if not has_typer: - raise ImportError( +if not HAS_TYPER: + msg = ( "\n\nYou're trying to use the FastStream CLI, " "\nbut you haven't installed the required dependencies." "\nPlease install them using the following command: " '\npip install "faststream[cli]"' ) + raise ImportError(msg) + +import warnings warnings.filterwarnings("default", category=ImportWarning, module="faststream") +from faststream._internal.cli.main import cli + if __name__ == "__main__": cli(prog_name="faststream") diff --git a/faststream/_compat.py b/faststream/_compat.py deleted file mode 100644 index 42b0c9adf3..0000000000 --- a/faststream/_compat.py +++ /dev/null @@ -1,184 +0,0 @@ -import json -import os -import sys -from importlib.metadata import version as get_version -from typing import Any, Callable, Dict, Mapping, Optional, Type, TypeVar, Union - -from pydantic import BaseModel as BaseModel -from pydantic.version import VERSION as PYDANTIC_VERSION - -from faststream.types import AnyDict - -IS_WINDOWS = ( - sys.platform == "win32" or sys.platform == "cygwin" or sys.platform == "msys" -) - - -ModelVar = TypeVar("ModelVar", bound=BaseModel) - - -def is_test_env() -> bool: - return bool(os.getenv("PYTEST_CURRENT_TEST")) - - -json_dumps: Callable[..., bytes] -orjson: Any -ujson: Any - -try: - import orjson # type: ignore[no-redef] -except ImportError: - orjson = None - -try: - import ujson -except ImportError: - ujson = None - -if orjson: - json_loads = orjson.loads - json_dumps = orjson.dumps - -elif ujson: - json_loads = ujson.loads - - def json_dumps(*a: Any, **kw: Any) -> bytes: - return ujson.dumps(*a, **kw).encode() # type: ignore - -else: - json_loads = json.loads - - def json_dumps(*a: Any, **kw: Any) -> bytes: - return json.dumps(*a, **kw).encode() - - -JsonSchemaValue = Mapping[str, Any] - -major, minor, *_ = PYDANTIC_VERSION.split(".") -_PYDANTCI_MAJOR, _PYDANTIC_MINOR = int(major), int(minor) - -PYDANTIC_V2 = _PYDANTCI_MAJOR >= 2 - -if PYDANTIC_V2: - if _PYDANTIC_MINOR >= 4: - from pydantic.annotated_handlers import ( - GetJsonSchemaHandler as GetJsonSchemaHandler, - ) - from pydantic_core.core_schema import ( - with_info_plain_validator_function as with_info_plain_validator_function, - ) - else: - from pydantic._internal._annotated_handlers import ( # type: ignore[no-redef] - GetJsonSchemaHandler as GetJsonSchemaHandler, - ) - from pydantic_core.core_schema import ( - general_plain_validator_function as with_info_plain_validator_function, - ) - - from pydantic.fields import FieldInfo as FieldInfo - from pydantic_core import CoreSchema as CoreSchema - from pydantic_core import PydanticUndefined as PydanticUndefined - from pydantic_core import to_jsonable_python - - SCHEMA_FIELD = "json_schema_extra" - DEF_KEY = "$defs" - - def model_to_jsonable( - model: BaseModel, - **kwargs: Any, - ) -> Any: - return to_jsonable_python(model, **kwargs) - - def dump_json(data: Any) -> bytes: - return json_dumps(model_to_jsonable(data)) - - def get_model_fields(model: Type[BaseModel]) -> Dict[str, Any]: - return model.model_fields - - def model_to_json(model: BaseModel, **kwargs: Any) -> str: - return model.model_dump_json(**kwargs) - - def model_parse( - model: Type[ModelVar], data: Union[str, bytes], **kwargs: Any - ) -> ModelVar: - return model.model_validate_json(data, **kwargs) - - def model_schema(model: Type[BaseModel], **kwargs: Any) -> AnyDict: - return model.model_json_schema(**kwargs) - -else: - from pydantic.fields import FieldInfo as FieldInfo - from pydantic.json import pydantic_encoder - - GetJsonSchemaHandler = Any # type: ignore[assignment,misc] - CoreSchema = Any # type: ignore[assignment,misc] - - SCHEMA_FIELD = "schema_extra" - DEF_KEY = "definitions" - - PydanticUndefined = Ellipsis # type: ignore[assignment] - - def dump_json(data: Any) -> bytes: - return json_dumps(data, default=pydantic_encoder) - - def get_model_fields(model: Type[BaseModel]) -> Dict[str, Any]: - return model.__fields__ # type: ignore[return-value] - - def model_to_json(model: BaseModel, **kwargs: Any) -> str: - return model.json(**kwargs) - - def model_parse( - model: Type[ModelVar], data: Union[str, bytes], **kwargs: Any - ) -> ModelVar: - return model.parse_raw(data, **kwargs) - - def model_schema(model: Type[BaseModel], **kwargs: Any) -> AnyDict: - return model.schema(**kwargs) - - def model_to_jsonable( - model: BaseModel, - **kwargs: Any, - ) -> Any: - return json_loads(model.json(**kwargs)) - - # TODO: pydantic types misc - def with_info_plain_validator_function( # type: ignore[misc] - function: Callable[..., Any], - *, - ref: Optional[str] = None, - metadata: Any = None, - serialization: Any = None, - ) -> JsonSchemaValue: - return {} - - -major, *_ = get_version("anyio").split(".") -_ANYIO_MAJOR = int(major) -ANYIO_V3 = _ANYIO_MAJOR == 3 - - -if ANYIO_V3: - from anyio import ExceptionGroup as ExceptionGroup # type: ignore[attr-defined] -else: - if sys.version_info < (3, 11): - from exceptiongroup import ( - ExceptionGroup as ExceptionGroup, - ) - else: - ExceptionGroup = ExceptionGroup - - -uvicorn: Any -UvicornMultiprocess: Any - -try: - import uvicorn - from uvicorn.supervisors.multiprocess import ( - Multiprocess as UvicornMultiprocess, - ) -except ImportError: - uvicorn = None - UvicornMultiprocess = None - HAS_UVICORN = False -else: - HAS_UVICORN = True diff --git a/faststream/_internal/_compat.py b/faststream/_internal/_compat.py new file mode 100644 index 0000000000..1693eba89c --- /dev/null +++ b/faststream/_internal/_compat.py @@ -0,0 +1,266 @@ +import json +import sys +import warnings +from collections import UserString +from collections.abc import Callable, Iterable, Mapping +from importlib.metadata import version as get_version +from importlib.util import find_spec +from typing import ( + Any, + TypeVar, +) + +from pydantic import BaseModel +from pydantic.version import VERSION as PYDANTIC_VERSION + +from faststream._internal.basic_types import AnyDict + +IS_WINDOWS = ( + sys.platform == "win32" or sys.platform == "cygwin" or sys.platform == "msys" +) + +__all__ = ( + "HAS_TYPER", + "PYDANTIC_V2", + "BaseModel", + "CoreSchema", + "EmailStr", + "ExceptionGroup", + "GetJsonSchemaHandler", + "PydanticUndefined", + "json_dumps", + "json_loads", + "with_info_plain_validator_function", +) + +try: + HAS_TYPER = find_spec("typer") is not None +except ImportError: + HAS_TYPER = False + + +json_dumps: Callable[..., bytes] +orjson: Any +ujson: Any + +try: + import orjson +except ImportError: + orjson = None + +try: + import ujson +except ImportError: + ujson = None + +if orjson: + json_loads = orjson.loads + json_dumps = orjson.dumps + +elif ujson: + json_loads = ujson.loads + + def json_dumps(*a: Any, **kw: Any) -> bytes: + return ujson.dumps(*a, **kw).encode() # type: ignore[no-any-return] + +else: + json_loads = json.loads + + def json_dumps(*a: Any, **kw: Any) -> bytes: + return json.dumps(*a, **kw).encode() + + +ModelVar = TypeVar("ModelVar", bound=BaseModel) + +JsonSchemaValue = Mapping[str, Any] +major, minor, *_ = PYDANTIC_VERSION.split(".") +_PYDANTCI_MAJOR, _PYDANTIC_MINOR = int(major), int(minor) + +PYDANTIC_V2 = _PYDANTCI_MAJOR >= 2 + +if PYDANTIC_V2: + if _PYDANTIC_MINOR >= 4: + from pydantic.annotated_handlers import ( + GetJsonSchemaHandler, + ) + from pydantic_core.core_schema import ( + with_info_plain_validator_function, + ) + else: + from pydantic._internal._annotated_handlers import ( # type: ignore[no-redef] + GetJsonSchemaHandler, + ) + from pydantic_core.core_schema import ( + general_plain_validator_function as with_info_plain_validator_function, + ) + + from pydantic_core import CoreSchema, PydanticUndefined, to_jsonable_python + + SCHEMA_FIELD = "json_schema_extra" + DEF_KEY = "$defs" + + def model_to_jsonable( + model: BaseModel, + **kwargs: Any, + ) -> Any: + return to_jsonable_python(model, **kwargs) + + def dump_json(data: Any) -> bytes: + return json_dumps(model_to_jsonable(data)) + + def get_model_fields(model: type[BaseModel]) -> AnyDict: + return model.__pydantic_fields__ + + def model_to_json(model: BaseModel, **kwargs: Any) -> str: + return model.model_dump_json(**kwargs) + + def model_parse( + model: type[ModelVar], + data: str | bytes, + **kwargs: Any, + ) -> ModelVar: + return model.model_validate_json(data, **kwargs) + + def model_schema(model: type[BaseModel], **kwargs: Any) -> AnyDict: + return model.model_json_schema(**kwargs) + +else: + from pydantic.json import pydantic_encoder + + GetJsonSchemaHandler = Any # type: ignore[assignment,misc] + CoreSchema = Any # type: ignore[assignment,misc] + + SCHEMA_FIELD = "schema_extra" + DEF_KEY = "definitions" + + PydanticUndefined = Ellipsis # type: ignore[assignment] + + def dump_json(data: Any) -> bytes: + return json_dumps(data, default=pydantic_encoder) + + def get_model_fields(model: type[BaseModel]) -> AnyDict: + return model.__fields__ # type: ignore[return-value] + + def model_to_json(model: BaseModel, **kwargs: Any) -> str: + return model.json(**kwargs) + + def model_parse( + model: type[ModelVar], + data: str | bytes, + **kwargs: Any, + ) -> ModelVar: + return model.parse_raw(data, **kwargs) + + def model_schema(model: type[BaseModel], **kwargs: Any) -> AnyDict: + return model.schema(**kwargs) + + def model_to_jsonable( + model: BaseModel, + **kwargs: Any, + ) -> Any: + return json_loads(model.json(**kwargs)) + + # TODO: pydantic types misc + def with_info_plain_validator_function( # type: ignore[misc] + function: Callable[..., Any], + *, + ref: str | None = None, + metadata: Any = None, + serialization: Any = None, + ) -> JsonSchemaValue: + return {} + + +major, *_ = get_version("anyio").split(".") +_ANYIO_MAJOR = int(major) +ANYIO_V3 = _ANYIO_MAJOR == 3 + + +if ANYIO_V3: + from anyio import ExceptionGroup # type: ignore[attr-defined] +elif sys.version_info >= (3, 11): + ExceptionGroup = ExceptionGroup # noqa: PLW0127 +else: + from exceptiongroup import ExceptionGroup + +try: + import email_validator + + if email_validator is None: + raise ImportError + from pydantic import EmailStr +except ImportError: # pragma: no cover + # NOTE: EmailStr mock was copied from the FastAPI + # https://github.com/tiangolo/fastapi/blob/master/fastapi/openapi/models.py#24 + class EmailStr(UserString): # type: ignore[no-redef] + """EmailStr is a string that should be an email. + + Note: EmailStr mock was copied from the FastAPI: + https://github.com/tiangolo/fastapi/blob/master/fastapi/openapi/models.py#24 + """ + + @classmethod + def __get_validators__(cls) -> Iterable[Callable[..., Any]]: + """Returns the validators for the EmailStr class.""" + yield cls.validate + + @classmethod + def validate(cls, v: Any) -> str: + """Validates the EmailStr class.""" + warnings.warn( + "email-validator bot installed, email fields will be treated as str.\n" + "To install, run: pip install email-validator", + category=RuntimeWarning, + stacklevel=1, + ) + return str(v) + + @classmethod + def _validate(cls, __input_value: Any, _: Any) -> str: + warnings.warn( + "email-validator bot installed, email fields will be treated as str.\n" + "To install, run: pip install email-validator", + category=RuntimeWarning, + stacklevel=1, + ) + return str(__input_value) + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + """Returns the JSON schema for the EmailStr class. + + Args: + core_schema : the core schema + handler : the handler + """ + return {"type": "string", "format": "email"} + + @classmethod + def __get_pydantic_core_schema__( + cls, + source: type[Any], + handler: Callable[[Any], CoreSchema], + ) -> JsonSchemaValue: + """Returns the core schema for the EmailStr class. + + Args: + source : the source + handler : the handler + """ + return with_info_plain_validator_function(cls._validate) + + +uvicorn: Any + +try: + import uvicorn + + HAS_UVICORN = True + +except ImportError: + uvicorn = None + HAS_UVICORN = False diff --git a/faststream/_internal/application.py b/faststream/_internal/application.py index 1074c44c54..0039d46854 100644 --- a/faststream/_internal/application.py +++ b/faststream/_internal/application.py @@ -1,212 +1,310 @@ import logging -from abc import ABC, abstractmethod +from abc import abstractmethod +from collections.abc import AsyncIterator, Callable, Sequence +from contextlib import asynccontextmanager from typing import ( TYPE_CHECKING, Any, - Callable, - Dict, - List, Optional, - Sequence, TypeVar, - Union, ) -import anyio from typing_extensions import ParamSpec -from faststream.asyncapi.proto import AsyncAPIApplication -from faststream.log.logging import logger -from faststream.utils import apply_types, context -from faststream.utils.functions import drop_response_type, fake_context, to_async - -P_HookParams = ParamSpec("P_HookParams") -T_HookReturn = TypeVar("T_HookReturn") +from faststream._internal.di import FastDependsConfig +from faststream._internal.logger import logger +from faststream._internal.utils import apply_types +from faststream._internal.utils.functions import ( + drop_response_type, + fake_context, + to_async, +) +from faststream.exceptions import SetupError if TYPE_CHECKING: - from faststream.asyncapi.schema import ( - Contact, - ContactDict, - ExternalDocs, - ExternalDocsDict, - License, - LicenseDict, - Tag, - TagDict, - ) - from faststream.broker.core.usecase import BrokerUsecase - from faststream.types import ( - AnyDict, - AnyHttpUrl, + from faststream._internal.basic_types import ( + AnyCallable, AsyncFunc, Lifespan, LoggerProto, SettingField, ) + from faststream._internal.broker import BrokerUsecase + from faststream._internal.context import ContextRepo + + +try: + from pydantic import ValidationError as PValidation + + from faststream.exceptions import StartupValidationError + + @asynccontextmanager + async def catch_startup_validation_error() -> AsyncIterator[None]: + try: + yield + except PValidation as e: + missed_fields = [] + invalid_fields = [] + for x in e.errors(): + location = str(x["loc"][0]) + if x["type"] == "missing": + missed_fields.append(location) + else: + invalid_fields.append(location) + raise StartupValidationError( + missed_fields=missed_fields, + invalid_fields=invalid_fields, + ) from e -class Application(ABC, AsyncAPIApplication): +except ImportError: + catch_startup_validation_error = fake_context + + +P_HookParams = ParamSpec("P_HookParams") +T_HookReturn = TypeVar("T_HookReturn") + + +class StartAbleApplication: def __init__( self, broker: Optional["BrokerUsecase[Any, Any]"] = None, + /, + config: Optional["FastDependsConfig"] = None, + ) -> None: + self._init_setupable_( + broker, + config=config, + ) + + @property + def context(self) -> "ContextRepo": + return self.config.context + + def _init_setupable_( # noqa: PLW3201 + self, + broker: Optional["BrokerUsecase[Any, Any]"] = None, + /, + config: Optional["FastDependsConfig"] = None, + ) -> None: + self.config = config or FastDependsConfig() + + self.brokers = [broker] if broker else [] + + for b in self.brokers: + b._update_fd_config(self.config) + + async def _start_broker(self) -> None: + assert self.brokers, "You should setup a broker" + for b in self.brokers: + await b.start() + + @property + def broker(self) -> Optional["BrokerUsecase[Any, Any]"]: + return self.brokers[0] if self.brokers else None + + def set_broker(self, broker: "BrokerUsecase[Any, Any]") -> None: + """Set already existed App object broker. + + Useful then you create/init broker in `on_startup` hook. + """ + if self.brokers: + msg = f"`{self}` already has a broker. You can't use multiple brokers until 1.0.0 release." + raise SetupError(msg) + + self.brokers.append(broker) + + +class Application(StartAbleApplication): + def __init__( + self, + broker: Optional["BrokerUsecase[Any, Any]"] = None, + /, + config: Optional["FastDependsConfig"] = None, logger: Optional["LoggerProto"] = logger, lifespan: Optional["Lifespan"] = None, - # AsyncAPI args, - title: str = "FastStream", - version: str = "0.1.0", - description: str = "", - 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, - external_docs: Optional[ - Union["ExternalDocs", "ExternalDocsDict", "AnyDict"] - ] = None, - identifier: Optional[str] = None, - on_startup: Sequence[Callable[P_HookParams, T_HookReturn]] = (), - after_startup: Sequence[Callable[P_HookParams, T_HookReturn]] = (), - on_shutdown: Sequence[Callable[P_HookParams, T_HookReturn]] = (), - after_shutdown: Sequence[Callable[P_HookParams, T_HookReturn]] = (), + on_startup: Sequence["AnyCallable"] = (), + after_startup: Sequence["AnyCallable"] = (), + on_shutdown: Sequence["AnyCallable"] = (), + after_shutdown: Sequence["AnyCallable"] = (), ) -> None: - context.set_global("app", self) + super().__init__(broker, config=config) + + self.context.set_global("app", self) - self._should_exit = False - self.broker = broker self.logger = logger - self.context = context - self._on_startup_calling: List[AsyncFunc] = [ - apply_types(to_async(x)) for x in on_startup + self._on_startup_calling: list[AsyncFunc] = [ + apply_types(to_async(x), context__=self.context) for x in on_startup ] - self._after_startup_calling: List[AsyncFunc] = [ - apply_types(to_async(x)) for x in after_startup + self._after_startup_calling: list[AsyncFunc] = [ + apply_types(to_async(x), context__=self.context) for x in after_startup ] - self._on_shutdown_calling: List[AsyncFunc] = [ - apply_types(to_async(x)) for x in on_shutdown + self._on_shutdown_calling: list[AsyncFunc] = [ + apply_types(to_async(x), context__=self.context) for x in on_shutdown ] - self._after_shutdown_calling: List[AsyncFunc] = [ - apply_types(to_async(x)) for x in after_shutdown + self._after_shutdown_calling: list[AsyncFunc] = [ + apply_types(to_async(x), context__=self.context) for x in after_shutdown ] if lifespan is not None: self.lifespan_context = apply_types( - func=lifespan, wrap_model=drop_response_type + func=lifespan, + wrap_model=drop_response_type, + context__=self.context, ) else: self.lifespan_context = fake_context - # AsyncAPI information - self.title = title - self.version = version - self.description = description - self.terms_of_service = terms_of_service - self.license = license - self.contact = contact - self.identifier = identifier - self.asyncapi_tags = tags - self.external_docs = external_docs + @abstractmethod + def exit(self) -> None: + """Stop application manually.""" + ... @abstractmethod async def run( self, log_level: int, - run_extra_options: Optional[Dict[str, "SettingField"]] = None, - sleep_time: float = 0.1, + run_extra_options: dict[str, "SettingField"] | None = None, ) -> None: ... - def set_broker(self, broker: "BrokerUsecase[Any, Any]") -> None: - """Set already existed App object broker. - - Useful then you create/init broker in `on_startup` hook. - """ - self.broker = broker + # Startup - def on_startup( + async def _startup( self, - func: Callable[P_HookParams, T_HookReturn], - ) -> Callable[P_HookParams, T_HookReturn]: - """Add hook running BEFORE broker connected. + log_level: int = logging.INFO, + run_extra_options: dict[str, "SettingField"] | None = None, + ) -> None: + """Private method calls `start` with logging.""" + async with self._startup_logging(log_level=log_level): + await self.start(**(run_extra_options or {})) - This hook also takes an extra CLI options as a kwargs. - """ - self._on_startup_calling.append(apply_types(to_async(func))) - return func + self.running = True - def on_shutdown( + async def start( self, - func: Callable[P_HookParams, T_HookReturn], - ) -> Callable[P_HookParams, T_HookReturn]: - """Add hook running BEFORE broker disconnected.""" - self._on_shutdown_calling.append(apply_types(to_async(func))) - return func + **run_extra_options: "SettingField", + ) -> None: + """Executes startup hooks and start broker.""" + async with self._start_hooks_context(**run_extra_options): + await self._start_broker() - def after_startup( + @asynccontextmanager + async def _start_hooks_context( self, - func: Callable[P_HookParams, T_HookReturn], - ) -> Callable[P_HookParams, T_HookReturn]: - """Add hook running AFTER broker connected.""" - self._after_startup_calling.append(apply_types(to_async(func))) - return func + **run_extra_options: "SettingField", + ) -> AsyncIterator[None]: + async with catch_startup_validation_error(): + for func in self._on_startup_calling: + await func(**run_extra_options) - def after_shutdown( + yield + + for func in self._after_startup_calling: + await func() + + @asynccontextmanager + async def _startup_logging( self, - func: Callable[P_HookParams, T_HookReturn], - ) -> Callable[P_HookParams, T_HookReturn]: - """Add hook running AFTER broker disconnected.""" - self._after_shutdown_calling.append(apply_types(to_async(func))) - return func + log_level: int = logging.INFO, + ) -> AsyncIterator[None]: + """Separated startup logging.""" + self._log( + log_level, + "FastStream app starting...", + ) - def exit(self) -> None: - """Stop application manually.""" - self._should_exit = True + yield - async def _main_loop(self, sleep_time: float) -> None: - """Run loop till exit signal.""" - while not self._should_exit: # noqa: ASYNC110 (requested by creator) - await anyio.sleep(sleep_time) + self._log( + log_level, + "FastStream app started successfully! To exit, press CTRL+C", + ) - async def start( - self, - **run_extra_options: "SettingField", - ) -> None: - """Executes startup hooks and start broker.""" - for func in self._on_startup_calling: - await func(**run_extra_options) + # Shutdown - if self.broker is not None: - await self.broker.start() + async def _shutdown(self, log_level: int = logging.INFO) -> None: + """Private method calls `stop` with logging.""" + async with self._shutdown_logging(log_level=log_level): + await self.stop() - for func in self._after_startup_calling: - await func() + self.running = False async def stop(self) -> None: """Executes shutdown hooks and stop broker.""" + async with self._shutdown_hooks_context(): + for broker in self.brokers: + await broker.close() + + @asynccontextmanager + async def _shutdown_hooks_context(self) -> AsyncIterator[None]: for func in self._on_shutdown_calling: await func() - if self.broker is not None: - await self.broker.close() + yield for func in self._after_shutdown_calling: await func() - async def _startup( + @asynccontextmanager + async def _shutdown_logging( self, log_level: int = logging.INFO, - run_extra_options: Optional[Dict[str, "SettingField"]] = None, - ) -> None: - self._log(log_level, "FastStream app starting...") - await self.start(**(run_extra_options or {})) - assert self.broker, "You should setup a broker" # nosec B101 - self._log( - log_level, "FastStream app started successfully! To exit, press CTRL+C" - ) - - async def _shutdown(self, log_level: int = logging.INFO) -> None: + ) -> AsyncIterator[None]: + """Separated startup logging.""" self._log(log_level, "FastStream app shutting down...") - await self.stop() + + yield + self._log(log_level, "FastStream app shut down gracefully.") + # Service methods + def _log(self, level: int, message: str) -> None: if self.logger is not None: self.logger.log(level, message) + + # Hooks + + def on_startup( + self, + func: Callable[P_HookParams, T_HookReturn], + ) -> Callable[P_HookParams, T_HookReturn]: + """Add hook running BEFORE broker connected. + + This hook also takes an extra CLI options as a kwargs. + """ + self._on_startup_calling.append( + apply_types(to_async(func), context__=self.context) + ) + return func + + def on_shutdown( + self, + func: Callable[P_HookParams, T_HookReturn], + ) -> Callable[P_HookParams, T_HookReturn]: + """Add hook running BEFORE broker disconnected.""" + self._on_shutdown_calling.append( + apply_types(to_async(func), context__=self.context) + ) + return func + + def after_startup( + self, + func: Callable[P_HookParams, T_HookReturn], + ) -> Callable[P_HookParams, T_HookReturn]: + """Add hook running AFTER broker connected.""" + self._after_startup_calling.append( + apply_types(to_async(func), context__=self.context) + ) + return func + + def after_shutdown( + self, + func: Callable[P_HookParams, T_HookReturn], + ) -> Callable[P_HookParams, T_HookReturn]: + """Add hook running AFTER broker disconnected.""" + self._after_shutdown_calling.append( + apply_types(to_async(func), context__=self.context) + ) + return func diff --git a/faststream/_internal/basic_types.py b/faststream/_internal/basic_types.py new file mode 100644 index 0000000000..e525245f23 --- /dev/null +++ b/faststream/_internal/basic_types.py @@ -0,0 +1,74 @@ +from collections.abc import Awaitable, Callable, Mapping, Sequence +from contextlib import AbstractAsyncContextManager +from datetime import datetime +from decimal import Decimal +from typing import ( + Any, + ClassVar, + Protocol, + TypeAlias, + TypeVar, +) + +from typing_extensions import ParamSpec + +AnyDict: TypeAlias = dict[str, Any] +AnyHttpUrl: TypeAlias = str + +F_Return = TypeVar("F_Return") +F_Spec = ParamSpec("F_Spec") + +AnyCallable: TypeAlias = Callable[..., Any] +NoneCallable: TypeAlias = Callable[..., None] +AsyncFunc: TypeAlias = Callable[..., Awaitable[Any]] +AsyncFuncAny: TypeAlias = Callable[[Any], Awaitable[Any]] + +DecoratedCallable: TypeAlias = AnyCallable +DecoratedCallableNone: TypeAlias = NoneCallable + +Decorator: TypeAlias = Callable[[AnyCallable], AnyCallable] + +JsonArray: TypeAlias = Sequence["DecodedMessage"] + +JsonTable: TypeAlias = dict[str, "DecodedMessage"] + +JsonDecodable: TypeAlias = bool | bytes | bytearray | float | int | str | None + +DecodedMessage: TypeAlias = JsonDecodable | JsonArray | JsonTable + +SendableArray: TypeAlias = Sequence["BaseSendableMessage"] + +SendableTable: TypeAlias = dict[str, "BaseSendableMessage"] + + +class StandardDataclass(Protocol): + """Protocol to check type is dataclass.""" + + __dataclass_fields__: ClassVar[AnyDict] + + +BaseSendableMessage: TypeAlias = JsonDecodable | Decimal | datetime | StandardDataclass | SendableTable | SendableArray | None + +try: + from faststream._internal._compat import BaseModel + + SendableMessage: TypeAlias = BaseModel | BaseSendableMessage + +except ImportError: + SendableMessage: TypeAlias = BaseSendableMessage # type: ignore[no-redef,misc] + +SettingField: TypeAlias = bool | str | list[bool | str] | list[str] | list[bool] | int | None + +Lifespan: TypeAlias = Callable[..., AbstractAsyncContextManager[None]] + + +class LoggerProto(Protocol): + def log( + self, + level: int, + msg: Any, + /, + *, + exc_info: Any = None, + extra: Mapping[str, Any] | None = None, + ) -> None: ... diff --git a/faststream/_internal/broker/__init__.py b/faststream/_internal/broker/__init__.py new file mode 100644 index 0000000000..98f73e923a --- /dev/null +++ b/faststream/_internal/broker/__init__.py @@ -0,0 +1,4 @@ +from .broker import BrokerUsecase +from .router import BrokerRouter + +__all__ = ("BrokerRouter", "BrokerUsecase") diff --git a/faststream/_internal/broker/abc_broker.py b/faststream/_internal/broker/abc_broker.py new file mode 100644 index 0000000000..3399532f13 --- /dev/null +++ b/faststream/_internal/broker/abc_broker.py @@ -0,0 +1,114 @@ +from abc import abstractmethod +from collections.abc import Iterable, Sequence +from typing import ( + TYPE_CHECKING, + Generic, +) + +from faststream._internal.configs import BrokerConfig, ConfigComposition +from faststream._internal.endpoint.publisher import PublisherProto +from faststream._internal.endpoint.subscriber import ( + SubscriberProto, +) +from faststream._internal.types import BrokerMiddleware, MsgType + +if TYPE_CHECKING: + from fast_depends.dependencies import Dependant + + +class FinalSubscriber( + SubscriberProto[MsgType], +): + @property + @abstractmethod + def call_name(self) -> str: + raise NotImplementedError + + +class FinalPublisher( + PublisherProto[MsgType], +): + pass + + +class Registrator(Generic[MsgType]): + """Basic class for brokers and routers. + + Contains subscribers & publishers registration logic only. + """ + + def __init__( + self, + *, + config: "BrokerConfig", + routers: Sequence["Registrator[MsgType]"], + ) -> None: + self.config = ConfigComposition(config) + self._parser = self.config.broker_parser + self._decoder = self.config.broker_decoder + + self._subscribers: list[FinalSubscriber[MsgType]] = [] + self._publishers: list[FinalPublisher[MsgType]] = [] + self.routers: list[Registrator] = [] + + self.include_routers(*routers) + + @property + def subscribers(self) -> list[FinalSubscriber[MsgType]]: + return self._subscribers + [sub for r in self.routers for sub in r.subscribers] + + @property + def publishers(self) -> list[FinalPublisher[MsgType]]: + return self._publishers + [pub for r in self.routers for pub in r.publishers] + + def add_middleware(self, middleware: "BrokerMiddleware[MsgType]") -> None: + """Append BrokerMiddleware to the end of middlewares list. + + Current middleware will be used as a most inner of already existed ones. + """ + self.config.add_middleware(middleware) + + @abstractmethod + def subscriber( + self, + subscriber: "FinalSubscriber[MsgType]", + ) -> "FinalSubscriber[MsgType]": + self._subscribers.append(subscriber) + return subscriber + + @abstractmethod + def publisher( + self, + publisher: "FinalPublisher[MsgType]", + ) -> "FinalPublisher[MsgType]": + self._publishers.append(publisher) + return publisher + + def include_router( + self, + router: "Registrator[MsgType]", + *, + prefix: str = "", + dependencies: Iterable["Dependant"] = (), + middlewares: Iterable["BrokerMiddleware[MsgType]"] = (), + include_in_schema: bool | None = None, + ) -> None: + """Includes a router in the current object.""" + if options_config := BrokerConfig( + prefix=prefix, + include_in_schema=include_in_schema, + broker_middlewares=middlewares, + broker_dependencies=dependencies, + ): + router.config.add_config(options_config) + + router.config.add_config(self.config) + self.routers.append(router) + + def include_routers( + self, + *routers: "Registrator[MsgType]", + ) -> None: + """Includes routers in the object.""" + for r in routers: + self.include_router(r) diff --git a/faststream/_internal/broker/broker.py b/faststream/_internal/broker/broker.py new file mode 100644 index 0000000000..bb4e79240e --- /dev/null +++ b/faststream/_internal/broker/broker.py @@ -0,0 +1,142 @@ +from abc import abstractmethod +from collections.abc import Sequence +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Optional, +) + +from fast_depends import Provider +from typing_extensions import Self + +from faststream._internal.types import ( + BrokerMiddleware, + ConnectionType, + MsgType, +) + +from .abc_broker import Registrator +from .pub_base import BrokerPublishMixin + +if TYPE_CHECKING: + from types import TracebackType + + from faststream._internal.configs import BrokerConfig + from faststream._internal.context.repository import ContextRepo + from faststream._internal.di import FastDependsConfig + from faststream._internal.producer import ProducerProto + from faststream.specification.schema import BrokerSpec + + +class BrokerUsecase( + Registrator[MsgType], + BrokerPublishMixin[MsgType], + Generic[MsgType, ConnectionType], +): + """Basic class for brokers-only. + + Extends `Registrator` by connection, publish and AsyncAPI behavior. + """ + + _connection: ConnectionType | None + + def __init__( + self, + *, + config: "BrokerConfig", + specification: "BrokerSpec", + routers: Sequence["Registrator[MsgType]"], + **connection_kwargs: Any, + ) -> None: + super().__init__( + routers=routers, + config=config, + ) + self.specification = specification + + self.running = False + + self._connection_kwargs = connection_kwargs + self._connection = None + + @property + def middlewares(self) -> Sequence["BrokerMiddleware[MsgType]"]: + return self.config.broker_middlewares + + @property + def _producer(self) -> "ProducerProto": + return self.config.producer + + @property + def context(self) -> "ContextRepo": + return self.config.fd_config.context + + @property + def provider(self) -> Provider: + return self.config.fd_config.provider + + async def __aenter__(self) -> "Self": + await self.connect() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: Optional["TracebackType"], + ) -> None: + await self.close(exc_type, exc_val, exc_tb) + + def _update_fd_config(self, config: "FastDependsConfig") -> None: + """Private method to change broker config state by outer application.""" + self.config.broker_config.fd_config = ( + config | self.config.broker_config.fd_config + ) + + async def start(self) -> None: + self._setup_logger() + + # TODO: filter by already running handlers after TestClient refactor + for sub in self.subscribers: + await sub.start() + + for pub in self.publishers: + await pub.start() + + self.running = True + + def _setup_logger(self) -> None: + for sub in self.subscribers: + log_context = sub.get_log_context(None) + log_context.pop("message_id", None) + self.config.logger.params_storage.register_subscriber(log_context) + + self.config.logger._setup(self.config.fd_config.context) + + async def connect(self) -> ConnectionType: + """Connect to a remote server.""" + if self._connection is None: + self._connection = await self._connect() + return self._connection + + @abstractmethod + async def _connect(self) -> ConnectionType: + raise NotImplementedError + + async def close( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> None: + """Closes the object.""" + for sub in self.subscribers: + await sub.close() + + self.running = False + + @abstractmethod + async def ping(self, timeout: float | None) -> bool: + """Check connection alive.""" + raise NotImplementedError diff --git a/faststream/_internal/broker/pub_base.py b/faststream/_internal/broker/pub_base.py new file mode 100644 index 0000000000..1f2dbeba71 --- /dev/null +++ b/faststream/_internal/broker/pub_base.py @@ -0,0 +1,105 @@ +from abc import abstractmethod +from collections.abc import Sequence +from functools import partial +from typing import TYPE_CHECKING, Any, Generic + +from faststream._internal.endpoint.utils import process_msg +from faststream._internal.types import MsgType +from faststream.message.source_type import SourceType + +if TYPE_CHECKING: + from faststream._internal.basic_types import SendableMessage + from faststream._internal.context import ContextRepo + from faststream._internal.producer import ProducerProto + from faststream._internal.types import BrokerMiddleware + from faststream.response import PublishCommand + + +class BrokerPublishMixin(Generic[MsgType]): + @property + @abstractmethod + def middlewares(self) -> Sequence["BrokerMiddleware[MsgType]"]: + raise NotImplementedError + + @property + @abstractmethod + def context(self) -> "ContextRepo": + raise NotImplementedError + + @abstractmethod + async def publish( + self, + message: "SendableMessage", + queue: str, + /, + ) -> Any: + raise NotImplementedError + + async def _basic_publish( + self, + cmd: "PublishCommand", + *, + producer: "ProducerProto", + ) -> Any: + publish = producer.publish + context = self.context # caches property + + for m in self.middlewares[::-1]: + publish = partial(m(None, context=context).publish_scope, publish) + + return await publish(cmd) + + @abstractmethod + async def publish_batch( + self, + *messages: "SendableMessage", + queue: str, + ) -> Any: + raise NotImplementedError + + async def _basic_publish_batch( + self, + cmd: "PublishCommand", + *, + producer: "ProducerProto", + ) -> Any: + publish = producer.publish_batch + context = self.context # caches property + + for m in self.middlewares[::-1]: + publish = partial(m(None, context=context).publish_scope, publish) + + return await publish(cmd) + + @abstractmethod + async def request( + self, + message: "SendableMessage", + queue: str, + /, + timeout: float = 0.5, + ) -> Any: + raise NotImplementedError + + async def _basic_request( + self, + cmd: "PublishCommand", + *, + producer: "ProducerProto", + ) -> Any: + request = producer.request + context = self.context # caches property + + for m in self.middlewares[::-1]: + request = partial(m(None, context=context).publish_scope, request) + + published_msg = await request(cmd) + + response_msg: Any = await process_msg( + msg=published_msg, + middlewares=(m(published_msg, context=context) for m in self.middlewares), + parser=producer._parser, + decoder=producer._decoder, + source_type=SourceType.RESPONSE, + ) + return response_msg diff --git a/faststream/_internal/broker/router.py b/faststream/_internal/broker/router.py new file mode 100644 index 0000000000..fa2ea37810 --- /dev/null +++ b/faststream/_internal/broker/router.py @@ -0,0 +1,76 @@ +from collections.abc import Callable, Iterable, Sequence +from typing import ( + TYPE_CHECKING, + Any, +) + +from faststream._internal.types import MsgType + +from .abc_broker import Registrator + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict + from faststream._internal.configs import BrokerConfig + + +class ArgsContainer: + """Class to store any arguments.""" + + __slots__ = ("args", "kwargs") + + args: Iterable[Any] + kwargs: "AnyDict" + + def __init__( + self, + *args: Any, + **kwargs: Any, + ) -> None: + self.args = args + self.kwargs = kwargs + + +class SubscriberRoute(ArgsContainer): + """A generic class to represent a broker route.""" + + __slots__ = ("call", "publishers") + + call: Callable[..., Any] + publishers: Iterable[Any] + + def __init__( + self, + call: Callable[..., Any], + *args: Any, + publishers: Iterable[ArgsContainer] = (), + **kwargs: Any, + ) -> None: + """Initialize a callable object with arguments and keyword arguments.""" + self.call = call + self.publishers = publishers + + super().__init__(*args, **kwargs) + + +class BrokerRouter(Registrator[MsgType]): + """A generic class representing a broker router.""" + + def __init__( + self, + *, + config: "BrokerConfig", + handlers: Iterable[SubscriberRoute], + routers: Sequence["Registrator[MsgType]"], + ) -> None: + super().__init__( + config=config, + routers=routers, + ) + + for h in handlers: + call = h.call + + for p in h.publishers: + call = self.publisher(*p.args, **p.kwargs)(call) + + self.subscriber(*h.args, **h.kwargs)(call) diff --git a/faststream/broker/core/__init__.py b/faststream/_internal/cli/__init__.py similarity index 100% rename from faststream/broker/core/__init__.py rename to faststream/_internal/cli/__init__.py diff --git a/faststream/_internal/cli/docs.py b/faststream/_internal/cli/docs.py new file mode 100644 index 0000000000..8d98678248 --- /dev/null +++ b/faststream/_internal/cli/docs.py @@ -0,0 +1,205 @@ +import json +import sys +import warnings +from contextlib import suppress +from pathlib import Path +from pprint import pformat +from typing import TYPE_CHECKING + +import typer +from pydantic import ValidationError + +from faststream._internal._compat import json_dumps, model_parse +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 ( + ApplicationSchema as SchemaV2_6, +) +from faststream.specification.asyncapi.v3_0_0.schema import ( + ApplicationSchema as SchemaV3, +) +from faststream.specification.base.specification import Specification + +from .options import ( + APP_DIR_OPTION, + FACTORY_OPTION, + RELOAD_EXTENSIONS_OPTION, + RELOAD_FLAG, +) + +if TYPE_CHECKING: + from collections.abc import Sequence + +docs_app = typer.Typer(pretty_exceptions_short=True) + + +@docs_app.command(name="serve") +def serve( + docs: str = typer.Argument( + ..., + help="[python_module:Specification] or [asyncapi.json/.yaml] - path to your application or documentation.", + show_default=False, + ), + host: str = typer.Option( + "localhost", + help="Documentation hosting address.", + ), + port: int = typer.Option( + 8000, + help="Documentation hosting port.", + ), + app_dir: str = APP_DIR_OPTION, + is_factory: bool = FACTORY_OPTION, + reload: bool = RELOAD_FLAG, + watch_extensions: list[str] = RELOAD_EXTENSIONS_OPTION, +) -> None: + """Serve project AsyncAPI schema.""" + if ":" in docs: + if app_dir: # pragma: no branch + sys.path.insert(0, app_dir) + + module, _ = import_from_string(docs, is_factory=is_factory) + + module_parent = module.parent + extra_extensions: Sequence[str] = watch_extensions + + else: + module_parent = Path.cwd() + schema_filepath = module_parent / docs + extra_extensions = (schema_filepath.suffix, *watch_extensions) + + if reload: + try: + from faststream._internal.cli.supervisors.watchfiles import WatchReloader + + except ImportError: + warnings.warn(INSTALL_WATCHFILES, category=ImportWarning, stacklevel=1) + _parse_and_serve(docs, host, port, is_factory) + + else: + WatchReloader( + target=_parse_and_serve, + args=(docs, host, port, is_factory), + reload_dirs=(str(module_parent),), + extra_extensions=extra_extensions, + ).run() + + else: + _parse_and_serve(docs, host, port, is_factory) + + +@docs_app.command(name="gen") +def gen( + asyncapi: str = typer.Argument( + ..., + help="[python_module:Specification] - path to your AsyncAPI object.", + show_default=False, + ), + yaml: bool = typer.Option( + False, + "--yaml", + is_flag=True, + help="Generate `asyncapi.yaml` schema.", + ), + out: str | None = typer.Option( + None, + "-o", + "--out", + help="Output filename.", + show_default="asyncapi.json/.yaml", + ), + debug: bool = typer.Option( + False, + "-d", + "--debug", + is_flag=True, + help="Do not save generated schema to file. Print it instead.", + ), + app_dir: str = APP_DIR_OPTION, + is_factory: bool = FACTORY_OPTION, +) -> None: + """Generate project AsyncAPI schema.""" + if app_dir: # pragma: no branch + sys.path.insert(0, app_dir) + + _, asyncapi_obj = import_from_string(asyncapi, is_factory=is_factory) + + assert isinstance(asyncapi_obj, Specification) # nosec B101 + + raw_schema = asyncapi_obj.schema + + if yaml: + try: + schema = raw_schema.to_yaml() + except ImportError as e: # pragma: no cover + typer.echo(INSTALL_YAML, err=True) + raise typer.Exit(1) from e + + filename = out or "asyncapi.yaml" + + if not debug: + Path(filename).write_text(schema, encoding="utf-8") + else: + schema = raw_schema.to_jsonable() + filename = out or "asyncapi.json" + + if not debug: + with Path(filename).open("w", encoding="utf-8") as f: + json.dump(schema, f, indent=2) + + else: + schema = pformat(schema) + + if debug: + typer.echo("Generated schema:\n") + typer.echo(schema, color=True) + + else: + typer.echo(f"Your project AsyncAPI scheme was placed to `{filename}`") + + +def _parse_and_serve( + docs: str, + host: str = "localhost", + port: int = 8000, + is_factory: bool = False, +) -> None: + if ":" in docs: + _, docs_obj = import_from_string(docs, is_factory=is_factory) + + assert isinstance(docs_obj, Specification) # nosec B101 + + raw_schema = docs_obj.schema + + else: + schema_filepath = Path.cwd() / docs + + if schema_filepath.suffix == ".json": + data = schema_filepath.read_bytes() + + elif schema_filepath.suffix in {".yaml", ".yml"}: + try: + import yaml + except ImportError as e: # pragma: no cover + typer.echo(INSTALL_YAML, err=True) + raise typer.Exit(1) from e + + with schema_filepath.open("r") as f: + schema = yaml.safe_load(f) + + data = json_dumps(schema) + + else: + msg = f"Unknown extension given - {docs}; Please provide app in format [python_module:Specification] or [asyncapi.yaml/.json] - path to your application or documentation" + raise ValueError(msg) + + for schema in (SchemaV3, SchemaV2_6): + with suppress(ValidationError): + raw_schema = model_parse(schema, data) + break + else: + typer.echo(SCHEMA_NOT_SUPPORTED.format(schema_filename=docs), err=True) + raise typer.Exit(1) + + serve_app(raw_schema, host, port) diff --git a/faststream/_internal/cli/main.py b/faststream/_internal/cli/main.py new file mode 100644 index 0000000000..1183de7642 --- /dev/null +++ b/faststream/_internal/cli/main.py @@ -0,0 +1,313 @@ +import logging +import sys +import warnings +from contextlib import suppress +from pathlib import Path +from typing import TYPE_CHECKING, Any, cast + +import anyio +import typer + +from faststream import FastStream +from faststream.__about__ import __version__ +from faststream._internal._compat import json_loads +from faststream._internal.application import Application +from faststream.asgi import AsgiFastStream +from faststream.exceptions import INSTALL_WATCHFILES, SetupError, StartupValidationError + +from .docs import docs_app +from .options import ( + APP_ARGUMENT, + APP_DIR_OPTION, + FACTORY_OPTION, + RELOAD_EXTENSIONS_OPTION, + RELOAD_FLAG, +) +from .utils.imports import import_from_string +from .utils.logs import ( + LogFiles, + LogLevels, + get_log_level, + set_log_config, + set_log_level, +) +from .utils.parser import parse_cli_args + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict, SettingField + from faststream._internal.broker import BrokerUsecase + +cli = typer.Typer(pretty_exceptions_short=True) +cli.add_typer(docs_app, name="docs", help="Documentations commands") + + +def version_callback(version: bool) -> None: + """Callback function for displaying version information.""" + if version: + import platform + + typer.echo( + f"Running FastStream {__version__} with {platform.python_implementation()} " + f"{platform.python_version()} on {platform.system()}", + ) + + raise typer.Exit + + +@cli.callback() +def main( + version: bool | None = typer.Option( + False, + "-v", + "--version", + callback=version_callback, + is_eager=True, + help="Show current platform, python and FastStream version.", + ), +) -> None: + """Generate, run and manage FastStream apps to greater development experience.""" + + +@cli.command( + context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, +) +def run( + ctx: typer.Context, + app: str = APP_ARGUMENT, + workers: int = typer.Option( + 1, + "-w", + "--workers", + show_default=False, + help="Run [workers] applications with process spawning.", + envvar="FASTSTREAM_WORKERS", + ), + app_dir: str = APP_DIR_OPTION, + is_factory: bool = FACTORY_OPTION, + reload: bool = RELOAD_FLAG, + watch_extensions: list[str] = RELOAD_EXTENSIONS_OPTION, + log_level: LogLevels = typer.Option( + LogLevels.notset, + "-l", + "--log-level", + case_sensitive=False, + help="Set selected level for FastStream and brokers logger objects.", + envvar="FASTSTREAM_LOG_LEVEL", + show_default=False, + ), + log_config: Path | None = typer.Option( + None, + "--log-config", + help=( + "Set file to configure logging. Support " + f"{', '.join(f'`{x.value}`' for x in LogFiles)} extensions." # noqa: B008 + ), + show_default=False, + ), +) -> None: + """Run [MODULE:APP] FastStream application.""" + if watch_extensions and not reload: + typer.echo( + "Extra reload extensions has no effect without `--reload` flag." + "\nProbably, you forgot it?", + ) + + app, extra = parse_cli_args(app, *ctx.args) + casted_log_level = get_log_level(log_level) + + if app_dir: # pragma: no branch + sys.path.insert(0, app_dir) + + # Should be imported after sys.path changes + module_path, app_obj = import_from_string(app, is_factory=is_factory) + app_obj = cast("Application", app_obj) + + args = (app, extra, is_factory, log_config, casted_log_level) + + if reload and workers > 1: + msg = "You can't use reload option with multiprocessing" + raise SetupError(msg) + if workers <= 1: + extra["worker_id"] = None + + if reload: + try: + from faststream._internal.cli.supervisors.watchfiles import WatchReloader + except ImportError: + warnings.warn(INSTALL_WATCHFILES, category=ImportWarning, stacklevel=1) + _run(*args) + + else: + reload_dirs = [] + if module_path: + reload_dirs.append(str(module_path)) + if app_dir != ".": + reload_dirs.append(app_dir) + + WatchReloader( + target=_run, + args=args, + reload_dirs=reload_dirs, + extra_extensions=watch_extensions, + ).run() + + elif workers > 1: + if isinstance(app_obj, FastStream): + from faststream._internal.cli.supervisors.multiprocess import Multiprocess + + Multiprocess( + target=_run, + args=(*args, logging.DEBUG), + workers=workers, + ).run() + + elif isinstance(app_obj, AsgiFastStream): + from faststream._internal.cli.supervisors.asgi_multiprocess import ( + ASGIMultiprocess, + ) + + ASGIMultiprocess( + target=app, + args=args, + workers=workers, + ).run() + + else: + msg = f"Unexpected app type, expected FastStream or AsgiFastStream, got: {type(app_obj)}." + raise typer.BadParameter(msg) + + else: + _run_imported_app( + app_obj, + extra_options=extra, + log_level=casted_log_level, + log_config=log_config, + ) + + +def _run( + # NOTE: we should pass `str` due FastStream is not picklable + app: str, + extra_options: dict[str, "SettingField"], + is_factory: bool, + log_config: Path | None, + log_level: int = logging.NOTSET, + app_level: int = logging.INFO, # option for reloader only +) -> None: + """Runs the specified application.""" + _, app_obj = import_from_string(app, is_factory=is_factory) + app_obj = cast("Application", app_obj) + _run_imported_app( + app_obj, + extra_options=extra_options, + log_level=log_level, + app_level=app_level, + log_config=log_config, + ) + + +def _run_imported_app( + app_obj: "Application", + extra_options: dict[str, "SettingField"], + log_config: Path | None, + log_level: int = logging.NOTSET, + app_level: int = logging.INFO, # option for reloader only +) -> None: + if not isinstance(app_obj, Application): + msg = f'Imported object "{app_obj}" must be "Application" type.' + raise typer.BadParameter( + msg, + ) + + if log_level > 0: + set_log_level(log_level, app_obj) + + if log_config is not None: + set_log_config(log_config) + + if sys.platform not in {"win32", "cygwin", "cli"}: # pragma: no cover + with suppress(ImportError): + import uvloop + + uvloop.install() + + try: + anyio.run( + app_obj.run, + app_level, + extra_options, + ) + + except StartupValidationError as startup_exc: + from faststream._internal.cli.utils.errors import draw_startup_errors + + draw_startup_errors(startup_exc) + sys.exit(1) + + +@cli.command( + context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, +) +def publish( + ctx: typer.Context, + app: str = APP_ARGUMENT, + message: str = typer.Argument( + ..., + help="JSON Message string to publish.", + show_default=False, + ), + rpc: bool = typer.Option( + False, + help="Enable RPC mode and system output.", + ), + is_factory: bool = FACTORY_OPTION, +) -> None: + """Publish a message using the specified broker in a FastStream application. + + This command publishes a message to a broker configured in a FastStream app instance. + It supports various brokers and can handle extra arguments specific to each broker type. + These are parsed and passed to the broker's publish method. + """ + app, extra = parse_cli_args(app, *ctx.args) + + publish_extra: AnyDict = extra.copy() + if "timeout" in publish_extra: + publish_extra["timeout"] = float(publish_extra["timeout"]) + + try: + _, app_obj = import_from_string(app, is_factory=is_factory) + + assert isinstance(app_obj, FastStream), app_obj # nosec B101 + + if not app_obj.broker: + msg = "Broker instance not found in the app." + raise ValueError(msg) + + result = anyio.run(publish_message, app_obj.broker, rpc, message, publish_extra) + + if rpc: + typer.echo(result) + + except Exception as e: + typer.echo(f"Publish error: {e}") + sys.exit(1) + + +async def publish_message( + broker: "BrokerUsecase[Any, Any]", + rpc: bool, + message: str, + extra: "AnyDict", +) -> Any: + with suppress(Exception): + message = json_loads(message) + + try: + async with broker: + if rpc: + return await broker.request(message, **extra) # type: ignore[call-arg] + return await broker.publish(message, **extra) # type: ignore[call-arg] + + except Exception as e: + typer.echo(f"Error when broker was publishing: {e!r}") + sys.exit(1) diff --git a/faststream/_internal/cli/options.py b/faststream/_internal/cli/options.py new file mode 100644 index 0000000000..2d31f8a8c6 --- /dev/null +++ b/faststream/_internal/cli/options.py @@ -0,0 +1,38 @@ +import typer + +FACTORY_OPTION = typer.Option( + False, + "-f", + "--factory", + help="Treat APP as an application factory.", +) + +RELOAD_FLAG = typer.Option( + False, + "-r", + "--reload", + is_flag=True, + help="Restart app at directory files changes.", +) + +APP_DIR_OPTION = typer.Option( + ".", + "--app-dir", + help=("Look for APP in the specified directory, by adding this to the PYTHONPATH."), + envvar="FASTSTREAM_APP_DIR", +) + +RELOAD_EXTENSIONS_OPTION = typer.Option( + (), + "--extension", + "--ext", + "--reload-extension", + "--reload-ext", + help="List of file extensions to watch by.", +) + +APP_ARGUMENT = typer.Argument( + ..., + help="[python_module:FastStream] - path to your application.", + show_default=False, +) diff --git a/faststream/broker/publisher/__init__.py b/faststream/_internal/cli/supervisors/__init__.py similarity index 100% rename from faststream/broker/publisher/__init__.py rename to faststream/_internal/cli/supervisors/__init__.py diff --git a/faststream/_internal/cli/supervisors/asgi_multiprocess.py b/faststream/_internal/cli/supervisors/asgi_multiprocess.py new file mode 100644 index 0000000000..38b0f8eb21 --- /dev/null +++ b/faststream/_internal/cli/supervisors/asgi_multiprocess.py @@ -0,0 +1,72 @@ +import inspect +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from faststream._internal._compat import HAS_UVICORN, uvicorn +from faststream.asgi.app import cast_uvicorn_params +from faststream.exceptions import INSTALL_UVICORN + +if TYPE_CHECKING: + from faststream._internal.basic_types import SettingField + +if HAS_UVICORN: + from uvicorn.supervisors.multiprocess import Multiprocess, Process + + class UvicornExtraConfig(uvicorn.Config): # type: ignore[misc] + def __init__( + self, + run_extra_options: dict[str, "SettingField"], + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self._run_extra_options = run_extra_options + + def load(self) -> None: + super().load() + self.loaded_app.app._run_extra_options = self._run_extra_options + + class UvicornMultiprocess(Multiprocess): + config: UvicornExtraConfig + + def init_processes(self) -> None: + for i in range(self.processes_num): + self.config._run_extra_options["worker_id"] = i + process = Process(self.config, self.target, self.sockets) + process.start() + self.processes.append(process) + + +class ASGIMultiprocess: + def __init__( + self, + target: str, + args: tuple[str, dict[str, str], bool, Path | None, int], + workers: int, + ) -> None: + _, run_extra_options, is_factory, _, log_level = args + self._target = target + self._run_extra_options = cast_uvicorn_params(run_extra_options or {}) + self._workers = workers + self._is_factory = is_factory + self._log_level = log_level + + def run(self) -> None: + if not HAS_UVICORN: + raise ImportError(INSTALL_UVICORN) + + config = UvicornExtraConfig( + app=self._target, + factory=self._is_factory, + log_level=self._log_level, + workers=self._workers, + **{ + key: v + for key, v in self._run_extra_options.items() + if key in set(inspect.signature(uvicorn.Config).parameters.keys()) + }, + run_extra_options=self._run_extra_options, + ) + server = uvicorn.Server(config) + sock = config.bind_socket() + UvicornMultiprocess(config, target=server.run, sockets=[sock]).run() diff --git a/faststream/_internal/cli/supervisors/basereload.py b/faststream/_internal/cli/supervisors/basereload.py new file mode 100644 index 0000000000..8cfa33e205 --- /dev/null +++ b/faststream/_internal/cli/supervisors/basereload.py @@ -0,0 +1,72 @@ +import os +import threading +from multiprocessing.context import SpawnProcess +from typing import TYPE_CHECKING, Any + +from faststream._internal.cli.supervisors.utils import get_subprocess, set_exit +from faststream._internal.logger import logger + +if TYPE_CHECKING: + from faststream._internal.basic_types import DecoratedCallable + + +class BaseReload: + """A base class for implementing a reloader process.""" + + _process: SpawnProcess + _target: "DecoratedCallable" + _args: tuple[Any, ...] + + reload_delay: float | None + should_exit: threading.Event + pid: int + reloader_name: str = "" + + def __init__( + self, + target: "DecoratedCallable", + args: tuple[Any, ...], + reload_delay: float | None = 0.5, + ) -> None: + self._target = target + self._args = args + + self.should_exit = threading.Event() + self.pid = os.getpid() + self.reload_delay = reload_delay + + set_exit(lambda *_: self.should_exit.set(), sync=True) + + def run(self) -> None: + self.startup() + while not self.should_exit.wait(self.reload_delay): + if self.should_restart(): # pragma: no branch + self.restart() + self.shutdown() + + def startup(self) -> None: + logger.info("Started reloader process [%s] using %s", self.pid, self.reloader_name) + self._process = self.start_process() + + def restart(self) -> None: + self._stop_process() + logger.info("Process successfully reloaded") + self._process = self.start_process() + + def shutdown(self) -> None: + self._stop_process() + logger.info("Stopping reloader process [%s]", self.pid) + + def _stop_process(self) -> None: + self._process.terminate() + self._process.join() + + def start_process(self, worker_id: int | None = None) -> SpawnProcess: + self._args[1]["worker_id"] = worker_id + process = get_subprocess(target=self._target, args=self._args) + process.start() + return process + + def should_restart(self) -> bool: + msg = "Reload strategies should override should_restart()" + raise NotImplementedError(msg) diff --git a/faststream/_internal/cli/supervisors/multiprocess.py b/faststream/_internal/cli/supervisors/multiprocess.py new file mode 100644 index 0000000000..dfa1318233 --- /dev/null +++ b/faststream/_internal/cli/supervisors/multiprocess.py @@ -0,0 +1,66 @@ +import signal +from typing import TYPE_CHECKING, Any + +from faststream._internal.cli.supervisors.basereload import BaseReload +from faststream._internal.logger import logger + +if TYPE_CHECKING: + from multiprocessing.context import SpawnProcess + + from faststream._internal.basic_types import DecoratedCallable + + +class Multiprocess(BaseReload): + """A class to represent a multiprocess.""" + + def __init__( + self, + target: "DecoratedCallable", + args: tuple[Any, ...], + workers: int, + reload_delay: float = 0.5, + ) -> None: + super().__init__(target, args, reload_delay) + + self.workers = workers + self.processes: list[SpawnProcess] = [] + + def startup(self) -> None: + logger.info("Started parent process [%s]", self.pid) + + for worker_id in range(self.workers): + process = self.start_process(worker_id=worker_id) + logger.info("Started child process %s [%s]", worker_id, process.pid) + self.processes.append(process) + + def shutdown(self) -> None: + for worker_id, process in enumerate(self.processes): + process.terminate() + logger.info("Stopping child process %s [%s]", worker_id, process.pid) + process.join() + + logger.info("Stopping parent process [%s]", self.pid) + + def restart(self) -> None: + active_processes = [] + + for worker_id, process in enumerate(self.processes): + if process.is_alive(): + active_processes.append(process) + continue + + log_msg = "Worker %s (pid:%s) exited with code %s." + if process.exitcode and abs(process.exitcode) == signal.SIGKILL: + log_msg += " Perhaps out of memory?" + logger.error(log_msg, worker_id, process.pid, process.exitcode) + + process.kill() + + new_process = self.start_process(worker_id=worker_id) + logger.info("Started child process [%s]", new_process.pid) + active_processes.append(new_process) + + self.processes = active_processes + + def should_restart(self) -> bool: + return not all(p.is_alive() for p in self.processes) diff --git a/faststream/_internal/cli/supervisors/utils.py b/faststream/_internal/cli/supervisors/utils.py new file mode 100644 index 0000000000..b3ee0dc614 --- /dev/null +++ b/faststream/_internal/cli/supervisors/utils.py @@ -0,0 +1,78 @@ +import asyncio +import multiprocessing +import os +import signal +import sys +from collections.abc import Callable +from contextlib import suppress +from typing import TYPE_CHECKING, Any, Optional + +from faststream._internal._compat import IS_WINDOWS + +if TYPE_CHECKING: + from multiprocessing.context import SpawnProcess + from types import FrameType + + from faststream._internal.basic_types import DecoratedCallableNone + +multiprocessing.allow_connection_pickling() +spawn = multiprocessing.get_context("spawn") + + +HANDLED_SIGNALS = ( + signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. + signal.SIGTERM, # Unix signal 15. Sent by `kill `. +) +if IS_WINDOWS: # pragma: py-not-win32 + HANDLED_SIGNALS += (signal.SIGBREAK,) # Windows signal 21. Sent by Ctrl+Break. + + +def set_exit( + func: Callable[[int, Optional["FrameType"]], Any], + *, + sync: bool = False, +) -> None: + """Set exit handler for signals. + + Args: + func: A callable object that takes an integer and an optional frame type as arguments and returns any value. + sync: set sync or async signal callback. + """ + if not sync: + with suppress(NotImplementedError): + loop = asyncio.get_event_loop() + + for sig in HANDLED_SIGNALS: + loop.add_signal_handler(sig, func, sig, None) + + return + + # Windows or sync mode + for sig in HANDLED_SIGNALS: + signal.signal(sig, func) + + +def get_subprocess(target: "DecoratedCallableNone", args: Any) -> "SpawnProcess": + """Spawn a subprocess.""" + stdin_fileno: int | None + try: + stdin_fileno = sys.stdin.fileno() + except OSError: + stdin_fileno = None + + return spawn.Process( + target=subprocess_started, + args=args, + kwargs={"t": target, "stdin_fileno": stdin_fileno}, + ) + + +def subprocess_started( + *args: Any, + t: "DecoratedCallableNone", + stdin_fileno: int | None, +) -> None: + """Start a subprocess.""" + if stdin_fileno is not None: # pragma: no cover + sys.stdin = os.fdopen(stdin_fileno) + t(*args) diff --git a/faststream/cli/supervisors/watchfiles.py b/faststream/_internal/cli/supervisors/watchfiles.py similarity index 83% rename from faststream/cli/supervisors/watchfiles.py rename to faststream/_internal/cli/supervisors/watchfiles.py index 9b70dbff5b..792f1bc37b 100644 --- a/faststream/cli/supervisors/watchfiles.py +++ b/faststream/_internal/cli/supervisors/watchfiles.py @@ -1,13 +1,14 @@ +from collections.abc import Sequence from pathlib import Path -from typing import TYPE_CHECKING, Any, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Any import watchfiles -from faststream.cli.supervisors.basereload import BaseReload -from faststream.log import logger +from faststream._internal.cli.supervisors.basereload import BaseReload +from faststream._internal.logger import logger if TYPE_CHECKING: - from faststream.types import DecoratedCallable + from faststream._internal.basic_types import DecoratedCallable class ExtendedFilter(watchfiles.PythonFilter): @@ -16,7 +17,7 @@ class ExtendedFilter(watchfiles.PythonFilter): def __init__( self, *, - ignore_paths: Optional[Sequence[Union[str, Path]]] = None, + ignore_paths: Sequence[str | Path] | None = None, extra_extensions: Sequence[str] = (), ) -> None: super().__init__(ignore_paths=ignore_paths, extra_extensions=extra_extensions) @@ -38,8 +39,8 @@ class WatchReloader(BaseReload): def __init__( self, target: "DecoratedCallable", - args: Tuple[Any, ...], - reload_dirs: Sequence[Union[Path, str]], + args: tuple[Any, ...], + reload_dirs: Sequence[Path | str], reload_delay: float = 0.3, extra_extensions: Sequence[str] = (), ) -> None: diff --git a/faststream/broker/subscriber/__init__.py b/faststream/_internal/cli/utils/__init__.py similarity index 100% rename from faststream/broker/subscriber/__init__.py rename to faststream/_internal/cli/utils/__init__.py diff --git a/faststream/_internal/cli/utils/errors.py b/faststream/_internal/cli/utils/errors.py new file mode 100644 index 0000000000..c9d1eb0721 --- /dev/null +++ b/faststream/_internal/cli/utils/errors.py @@ -0,0 +1,38 @@ +from faststream.exceptions import StartupValidationError + + +def draw_startup_errors(startup_exc: StartupValidationError) -> None: + from click.exceptions import BadParameter, MissingParameter + from typer.core import TyperOption + + def draw_error(click_exc: BadParameter) -> None: + try: + from typer import rich_utils + + rich_utils.rich_format_error(click_exc) + except ImportError: + click_exc.show() + + for field in startup_exc.invalid_fields: + draw_error( + BadParameter( + message=( + "extra option in your application " + "`lifespan/on_startup` hook has a wrong type." + ), + param=TyperOption(param_decls=[f"--{field}"]), + ), + ) + + if startup_exc.missed_fields: + draw_error( + MissingParameter( + message=( + "You registered extra options in your application " + "`lifespan/on_startup` hook, but does not set in CLI." + ), + param=TyperOption( + param_decls=[f"--{x}" for x in startup_exc.missed_fields], + ), + ), + ) diff --git a/faststream/_internal/cli/utils/imports.py b/faststream/_internal/cli/utils/imports.py new file mode 100644 index 0000000000..860b69a42a --- /dev/null +++ b/faststream/_internal/cli/utils/imports.py @@ -0,0 +1,129 @@ +import importlib +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + +import typer + +from faststream.exceptions import SetupError + + +def import_from_string( + import_str: str, + *, + is_factory: bool = False, +) -> tuple[Path, object]: + module_path, instance = _import_object_or_factory(import_str) + + if is_factory: + if callable(instance): + instance = instance() + else: + msg = f'"{instance}" is not a factory.' + raise typer.BadParameter(msg) + + return module_path, instance + + +def _import_object_or_factory(import_str: str) -> tuple[Path, object]: + """Import FastStream application from module specified by a string.""" + if not isinstance(import_str, str): + msg = "Given value is not of type string" + raise typer.BadParameter(msg) + + module_str, _, attrs_str = import_str.partition(":") + if not module_str or not attrs_str: + msg = f'Import string "{import_str}" must be in format ":"' + raise typer.BadParameter( + msg, + ) + + try: + module = importlib.import_module( # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import + module_str, + ) + + except ModuleNotFoundError: + module_path, import_obj_name = _get_obj_path(import_str) + instance = _try_import_app(module_path, import_obj_name) + + else: + attr = module + try: + for attr_str in attrs_str.split("."): + attr = getattr(attr, attr_str) + instance = attr + + except AttributeError as e: + typer.echo(e, err=True) + msg = f'Attribute "{attrs_str}" not found in module "{module_str}".' + raise typer.BadParameter( + msg, + ) from e + + if module.__file__: + module_path = Path(module.__file__).resolve().parent + else: + module_path = Path.cwd() + + return module_path, instance + + +def _try_import_app(module: Path, app: str) -> object: + """Tries to import a FastStream app from a module.""" + try: + app_object = _import_object(module, app) + + except FileNotFoundError as e: + typer.echo(e, err=True) + msg = ( + "Please, input module like [python_file:docs_object] or [module:attribute]" + ) + raise typer.BadParameter( + msg, + ) from e + + else: + return app_object + + +def _import_object(module: Path, app: str) -> object: + """Import an object from a module.""" + spec = spec_from_file_location( + "mode", + f"{module}.py", + submodule_search_locations=[str(module.parent.absolute())], + ) + + if spec is None: # pragma: no cover + raise FileNotFoundError(module) + + mod = module_from_spec(spec) + loader = spec.loader + + if loader is None: # pragma: no cover + msg = f"{spec} has no loader" + raise SetupError(msg) + + loader.exec_module(mod) + + try: + obj = getattr(mod, app) + except AttributeError as e: + raise FileNotFoundError(module) from e + + return obj + + +def _get_obj_path(obj_path: str) -> tuple[Path, str]: + """Get the application path.""" + if ":" not in obj_path: + msg = f"`{obj_path}` is not a path to object" + raise SetupError(msg) + + module, app_name = obj_path.split(":", 2) + + mod_path = Path.cwd() + for i in module.split("."): + mod_path /= i + + return mod_path, app_name diff --git a/faststream/_internal/cli/utils/logs.py b/faststream/_internal/cli/utils/logs.py new file mode 100644 index 0000000000..a14f3055da --- /dev/null +++ b/faststream/_internal/cli/utils/logs.py @@ -0,0 +1,148 @@ +import json +import logging +import logging.config +from collections import defaultdict +from enum import Enum +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import typer + +from faststream.exceptions import INSTALL_TOML, INSTALL_YAML + +if TYPE_CHECKING: + from faststream._internal.application import Application + + +class LogLevels(str, Enum): + """A class to represent log levels. + + Attributes: + critical : critical log level + error : error log level + warning : warning log level + info : info log level + debug : debug log level + """ + + critical = "critical" + fatal = "fatal" + error = "error" + warning = "warning" + warn = "warn" + info = "info" + debug = "debug" + notset = "notset" + + +LOG_LEVELS: defaultdict[str, int] = defaultdict( + lambda: logging.INFO, + critical=logging.CRITICAL, + fatal=logging.FATAL, + error=logging.ERROR, + warning=logging.WARNING, + warn=logging.WARNING, + info=logging.INFO, + debug=logging.DEBUG, + notset=logging.NOTSET, +) + + +class LogFiles(str, Enum): + """The class to represent supported log configuration files.""" + + json = ".json" + yaml = ".yaml" + yml = ".yml" + toml = ".toml" + + +def get_log_level(level: LogLevels | str | int) -> int: + """Get the log level. + + Args: + level: The log level to get. Can be an integer, a LogLevels enum value, or a string. + + Returns: + The log level as an integer. + + """ + if isinstance(level, int): + return level + + if isinstance(level, LogLevels): + return LOG_LEVELS[level.value] + + if isinstance(level, str): # pragma: no branch + return LOG_LEVELS[level.lower()] + + return None + + +def set_log_level(level: int, app: "Application") -> None: + """Sets the log level for an application.""" + if app.logger and getattr(app.logger, "setLevel", None): + app.logger.setLevel(level) # type: ignore[attr-defined] + + for broker in app.brokers: + broker.config.logger.set_level(level) + + +def _get_json_config(file: Path) -> dict[str, Any] | Any: + """Parse json config file to dict.""" + with file.open("r") as config_file: + return json.load(config_file) + + +def _get_yaml_config(file: Path) -> dict[str, Any] | Any: + """Parse yaml config file to dict.""" + try: + import yaml + except ImportError as e: + typer.echo(INSTALL_YAML, err=True) + raise typer.Exit(1) from e + + with file.open("r") as config_file: + return yaml.safe_load(config_file) + + +def _get_toml_config(file: Path) -> dict[str, Any] | Any: + """Parse toml config file to dict.""" + try: + import tomllib + except ImportError: + try: + import tomli as tomllib + except ImportError as e: + typer.echo(INSTALL_TOML, err=True) + raise typer.Exit(1) from e + + with file.open("rb") as config_file: + return tomllib.load(config_file) + + +def _get_log_config(file: Path) -> dict[str, Any] | Any: + """Read dict config from file.""" + if not file.exists(): + msg = f"File {file} specified to --log-config not found" + raise ValueError(msg) + + file_format = file.suffix + + if file_format == LogFiles.json: + logging_config = _get_json_config(file) + elif file_format in {LogFiles.yaml, LogFiles.yml}: + logging_config = _get_yaml_config(file) + elif file_format == LogFiles.toml: + logging_config = _get_toml_config(file) + else: + msg = f"Format {file_format} specified to --log-config file is not supported" + raise ValueError(msg) + + return logging_config + + +def set_log_config(file: Path) -> None: + """Set the logging config from file.""" + configuration = _get_log_config(file) + logging.config.dictConfig(configuration) diff --git a/faststream/_internal/cli/utils/parser.py b/faststream/_internal/cli/utils/parser.py new file mode 100644 index 0000000000..72ecff5251 --- /dev/null +++ b/faststream/_internal/cli/utils/parser.py @@ -0,0 +1,65 @@ +import re +from functools import reduce +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from faststream._internal.basic_types import SettingField + + +def is_bind_arg(arg: str) -> bool: + """Determine whether the received argument refers to --bind. + + bind arguments are like: 0.0.0.0:8000, [::]:8000, fd://2, /tmp/socket.sock + + """ + bind_regex = re.compile(r":\d+$|:/+\d|:/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+") + return bool(bind_regex.search(arg)) + + +def parse_cli_args(*args: str) -> tuple[str, dict[str, "SettingField"]]: + """Parses command line arguments.""" + extra_kwargs: dict[str, SettingField] = {} + + k: str = "" + v: SettingField + + field_args: list[str] = [] + app = "" + for item in [ + *reduce( + lambda acc, x: acc + x.split("="), + args, + cast("list[str]", []), + ), + "-", + ]: + if ":" in item and not is_bind_arg(item): + app = item + + elif "-" in item: + if k: + k = k.strip().lstrip("-").replace("-", "_") + + if len(field_args) == 0: + v = not k.startswith("no_") + elif len(field_args) == 1: + v = field_args[0] + else: + v = field_args + + key = k.removeprefix("no_") + if (exists := extra_kwargs.get(key)) is not None: + v = [ + *(exists if isinstance(exists, list) else [exists]), + *(v if isinstance(v, list) else [v]), + ] + + extra_kwargs[key] = v + field_args = [] + + k = item + + else: + field_args.append(item) + + return app, extra_kwargs diff --git a/faststream/_internal/configs/__init__.py b/faststream/_internal/configs/__init__.py new file mode 100644 index 0000000000..6f72fa854e --- /dev/null +++ b/faststream/_internal/configs/__init__.py @@ -0,0 +1,15 @@ +from .broker import BrokerConfig, ConfigComposition +from .endpoint import PublisherUsecaseConfig, SubscriberUsecaseConfig +from .specification import ( + PublisherSpecificationConfig, + SpecificationConfig as SubscriberSpecificationConfig, +) + +__all__ = ( + "BrokerConfig", + "ConfigComposition", + "PublisherSpecificationConfig", + "PublisherUsecaseConfig", + "SubscriberSpecificationConfig", + "SubscriberUsecaseConfig", +) diff --git a/faststream/_internal/configs/broker.py b/faststream/_internal/configs/broker.py new file mode 100644 index 0000000000..5f8ccc83f0 --- /dev/null +++ b/faststream/_internal/configs/broker.py @@ -0,0 +1,121 @@ +from collections.abc import Iterable +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Optional + +from faststream._internal.di import FastDependsConfig +from faststream._internal.logger import LoggerState +from faststream._internal.producer import ProducerProto, ProducerUnset + +if TYPE_CHECKING: + from fast_depends.dependencies import Dependant + + from faststream._internal.basic_types import AnyDict + from faststream._internal.types import AsyncCallable, BrokerMiddleware + + +@dataclass(kw_only=True) +class BrokerConfig: + prefix: str = "" + include_in_schema: bool | None = True + + broker_middlewares: Iterable["BrokerMiddleware[Any]"] = () + broker_parser: Optional["AsyncCallable"] = None + broker_decoder: Optional["AsyncCallable"] = None + + producer: "ProducerProto" = field(default_factory=ProducerUnset) + logger: "LoggerState" = field(default_factory=LoggerState) + fd_config: "FastDependsConfig" = field(default_factory=FastDependsConfig) + + # subscriber options + broker_dependencies: Iterable["Dependant"] = () + graceful_timeout: float | None = None + extra_context: "AnyDict" = field(default_factory=dict) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(id: {id(self)})" + + def __bool__(self) -> bool: + return bool( + self.include_in_schema is not None + or self.broker_middlewares + or self.broker_dependencies + or self.prefix + ) + + def add_middleware(self, middleware: "BrokerMiddleware[Any]") -> None: + self.broker_middlewares = (*self.broker_middlewares, middleware) + + +class ConfigComposition: + def __init__(self, *configs: "BrokerConfig") -> None: + self.configs = configs + + @property + def broker_config(self) -> "BrokerConfig": + assert self.configs + return self.configs[0] + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({', '.join(repr(c) for c in self.configs)})" + + def add_config(self, config: "BrokerConfig") -> None: + self.configs = (config, *self.configs) + + # broker priorrity options + @property + def producer(self) -> "ProducerProto": + return self.broker_config.producer + + @property + def logger(self) -> "LoggerState": + return self.broker_config.logger + + @property + def fd_config(self) -> "FastDependsConfig": + return self.broker_config.fd_config + + @property + def graceful_timeout(self) -> float | None: + return self.broker_config.graceful_timeout + + def __getattr__(self, name: str) -> Any: + return getattr(self.broker_config, name) + + # first valuable option + @property + def broker_parser(self) -> Optional["AsyncCallable"]: + for c in self.configs: + if c.broker_parser: + return c.broker_parser + return None + + @property + def broker_decoder(self) -> Optional["AsyncCallable"]: + for c in self.configs: + if c.broker_decoder: + return c.broker_decoder + return None + + # merged options + @property + def extra_context(self) -> "AnyDict": + context: AnyDict = {} + for c in self.configs: + context |= c.extra_context + return context + + @property + def prefix(self) -> str: + return "".join(c.prefix for c in self.configs) + + @property + def include_in_schema(self) -> bool: + return all(c.include_in_schema is not False for c in self.configs) + + @property + def broker_middlewares(self) -> Iterable["BrokerMiddleware[Any]"]: + return [m for c in self.configs for m in c.broker_middlewares] + + @property + def broker_dependencies(self) -> Iterable["Dependant"]: + return [b for c in self.configs for b in c.broker_dependencies] diff --git a/faststream/_internal/configs/endpoint.py b/faststream/_internal/configs/endpoint.py new file mode 100644 index 0000000000..8ef9788e87 --- /dev/null +++ b/faststream/_internal/configs/endpoint.py @@ -0,0 +1,32 @@ +from collections.abc import Sequence +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from faststream._internal.constants import EMPTY +from faststream.middlewares import AckPolicy + +if TYPE_CHECKING: + from faststream._internal.types import PublisherMiddleware + + from .broker import BrokerConfig + + +@dataclass(kw_only=True) +class EndpointConfig: + _outer_config: "BrokerConfig" + + +@dataclass(kw_only=True) +class PublisherUsecaseConfig(EndpointConfig): + middlewares: Sequence["PublisherMiddleware"] + + +@dataclass(kw_only=True) +class SubscriberUsecaseConfig(EndpointConfig): + no_reply: bool = False + + _ack_policy: AckPolicy = field(default_factory=lambda: EMPTY, repr=False) + + @property + def ack_policy(self) -> AckPolicy: + raise NotImplementedError diff --git a/faststream/_internal/configs/specification.py b/faststream/_internal/configs/specification.py new file mode 100644 index 0000000000..58b9851b48 --- /dev/null +++ b/faststream/_internal/configs/specification.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from typing import Any + + +@dataclass(kw_only=True) +class SpecificationConfig: + title_: str | None + description_: str | None + + include_in_schema: bool = True + + +@dataclass(kw_only=True) +class PublisherSpecificationConfig(SpecificationConfig): + schema_: Any | None diff --git a/faststream/_internal/constants.py b/faststream/_internal/constants.py new file mode 100644 index 0000000000..c81916ed95 --- /dev/null +++ b/faststream/_internal/constants.py @@ -0,0 +1,25 @@ +from enum import Enum +from typing import Any + +ContentType = str + + +class ContentTypes(str, Enum): + """A class to represent content types.""" + + TEXT = "text/plain" + JSON = "application/json" + + +class EmptyPlaceholder: + def __repr__(self) -> str: + return "EMPTY" + + def __bool__(self) -> bool: + return False + + def __eq__(self, other: object) -> bool: + return isinstance(other, EmptyPlaceholder) + + +EMPTY: Any = EmptyPlaceholder() diff --git a/faststream/_internal/context/__init__.py b/faststream/_internal/context/__init__.py new file mode 100644 index 0000000000..f0a0b1d1cb --- /dev/null +++ b/faststream/_internal/context/__init__.py @@ -0,0 +1,7 @@ +from .context_type import Context +from .repository import ContextRepo + +__all__ = ( + "Context", + "ContextRepo", +) diff --git a/faststream/_internal/context/context_type.py b/faststream/_internal/context/context_type.py new file mode 100644 index 0000000000..58db5c0091 --- /dev/null +++ b/faststream/_internal/context/context_type.py @@ -0,0 +1,79 @@ +from collections.abc import Callable +from typing import Any + +from fast_depends.library import CustomField + +from faststream._internal.basic_types import AnyDict +from faststream._internal.constants import EMPTY + +from .resolve import resolve_context_by_name + + +class Context(CustomField): + """A class to represent a context. + + Attributes: + param_name : name of the parameter + + Methods: + __init__ : constructor method + use : method to use the context + """ + + param_name: str + + def __init__( + self, + real_name: str = "", + *, + default: Any = EMPTY, + initial: Callable[..., Any] | None = None, + cast: bool = False, + prefix: str = "", + ) -> None: + """Initialize the object. + + Args: + real_name: The real name of the object. + default: The default value of the object. + initial: The initial value builder. + cast: Whether to cast the object. + prefix: The prefix to be added to the name of the object. + + Raises: + TypeError: If the default value is not provided. + """ + self.name = real_name + self.default = default + self.prefix = prefix + self.initial = initial + super().__init__( + cast=cast, + required=(default is EMPTY), + ) + + def use(self, /, **kwargs: Any) -> AnyDict: + """Use the given keyword arguments. + + Args: + **kwargs: Keyword arguments to be used + + Returns: + A dictionary containing the updated keyword arguments + """ + name = f"{self.prefix}{self.name or self.param_name}" + + if EMPTY != ( # noqa: SIM300 + v := resolve_context_by_name( + name=name, + default=self.default, + initial=self.initial, + context=kwargs["context__"], + ) + ): + kwargs[self.param_name] = v + + else: + kwargs.pop(self.param_name, None) + + return kwargs diff --git a/faststream/utils/context/repository.py b/faststream/_internal/context/repository.py similarity index 85% rename from faststream/utils/context/repository.py rename to faststream/_internal/context/repository.py index 034d2a504c..61ba0a617e 100644 --- a/faststream/utils/context/repository.py +++ b/faststream/_internal/context/repository.py @@ -1,18 +1,18 @@ +from collections.abc import Iterator, Mapping from contextlib import contextmanager from contextvars import ContextVar, Token -from typing import Any, Dict, Iterator, Mapping +from typing import Any -from faststream.types import EMPTY, AnyDict -from faststream.utils.classes import Singleton +from faststream._internal.basic_types import AnyDict +from faststream._internal.constants import EMPTY +from faststream.exceptions import ContextError -__all__ = ("ContextRepo", "context") - -class ContextRepo(Singleton): +class ContextRepo: """A class to represent a context repository.""" _global_context: AnyDict - _scope_context: Dict[str, ContextVar[Any]] + _scope_context: dict[str, ContextVar[Any]] def __init__(self) -> None: """Initialize the class. @@ -92,12 +92,13 @@ def get_local(self, key: str, default: Any = None) -> Any: Returns: The value of the local variable. """ - value = default - if (context_var := self._scope_context.get(key)) is not None and ( - context_value := context_var.get() - ) is not EMPTY: - value = context_value - return value + if (context_var := self._scope_context.get(key)) is None: + return default + + if (context_value := context_var.get()) is EMPTY: + return default + + return context_value @contextmanager def scope(self, key: str, value: Any) -> Iterator[None]: @@ -131,19 +132,18 @@ def get(self, key: str, default: Any = None) -> Any: """ if (glob := self._global_context.get(key, EMPTY)) is EMPTY: return self.get_local(key, default) - else: - return glob + return glob - def __getattr__(self, __name: str) -> Any: + def __getattr__(self, name: str, /) -> Any: """This is a function that is part of a class. It is used to get an attribute value using the `__getattr__` method. Args: - __name: The name of the attribute to get. + name: The name of the attribute to get. Returns: The value of the attribute. """ - return self.get(__name) + return self.get(name) def resolve(self, argument: str) -> Any: """Resolve the context of an argument. @@ -160,7 +160,7 @@ def resolve(self, argument: str) -> Any: first, *keys = argument.split(".") if (v := self.get(first, EMPTY)) is EMPTY: - raise KeyError(f"`{self.context}` does not contains `{first}` key") + raise ContextError(self.context, first) for i in keys: v = v[i] if isinstance(v, Mapping) else getattr(v, i) @@ -170,6 +170,3 @@ def resolve(self, argument: str) -> Any: def clear(self) -> None: self._global_context = {"context": self} self._scope_context.clear() - - -context = ContextRepo() diff --git a/faststream/_internal/context/resolve.py b/faststream/_internal/context/resolve.py new file mode 100644 index 0000000000..7b038bb82d --- /dev/null +++ b/faststream/_internal/context/resolve.py @@ -0,0 +1,29 @@ +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + +from faststream._internal.constants import EMPTY + +if TYPE_CHECKING: + from .repository import ContextRepo + + +def resolve_context_by_name( + name: str, + default: Any, + initial: Callable[..., Any] | None, + context: "ContextRepo", +) -> Any: + value: Any = EMPTY + + try: + value = context.resolve(name) + + except (KeyError, AttributeError): + if EMPTY != default: # noqa: SIM300 + value = default + + elif initial is not None: + value = initial() + context.set_global(name, value) + + return value diff --git a/faststream/_internal/di/__init__.py b/faststream/_internal/di/__init__.py new file mode 100644 index 0000000000..0fe8cdb513 --- /dev/null +++ b/faststream/_internal/di/__init__.py @@ -0,0 +1,3 @@ +from .config import FastDependsConfig + +__all__ = ("FastDependsConfig",) diff --git a/faststream/_internal/di/config.py b/faststream/_internal/di/config.py new file mode 100644 index 0000000000..a277570790 --- /dev/null +++ b/faststream/_internal/di/config.py @@ -0,0 +1,48 @@ +from collections.abc import Callable, Sequence +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Optional + +from fast_depends import Provider + +from faststream._internal.constants import EMPTY +from faststream._internal.context import ContextRepo + +if TYPE_CHECKING: + from fast_depends.library.serializer import SerializerProto + + from faststream._internal.basic_types import Decorator + + +@dataclass(kw_only=True) +class FastDependsConfig: + use_fastdepends: bool = True + + provider: Optional["Provider"] = field(default_factory=Provider) + serializer: Optional["SerializerProto"] = field(default_factory=lambda: EMPTY) + + context: "ContextRepo" = field(default_factory=ContextRepo) + + # To patch injection by integrations + call_decorators: Sequence["Decorator"] = () + get_dependent: Callable[..., Any] | None = None + + @property + def _serializer(self) -> Optional["SerializerProto"]: + if self.serializer is EMPTY: + from fast_depends.pydantic import PydanticSerializer + + return PydanticSerializer() + + return self.serializer + + def __or__(self, value: "FastDependsConfig", /) -> "FastDependsConfig": + use_fd = False if not value.use_fastdepends else self.use_fastdepends + + return FastDependsConfig( + use_fastdepends=use_fd, + provider=value.provider or self.provider, + serializer=self.serializer or value.serializer, + context=self.context, + call_decorators=(*value.call_decorators, *self.call_decorators), + get_dependent=self.get_dependent or value.get_dependent, + ) diff --git a/faststream/broker/wrapper/__init__.py b/faststream/_internal/endpoint/__init__.py similarity index 100% rename from faststream/broker/wrapper/__init__.py rename to faststream/_internal/endpoint/__init__.py diff --git a/faststream/_internal/endpoint/call_wrapper.py b/faststream/_internal/endpoint/call_wrapper.py new file mode 100644 index 0000000000..13d9d4bb84 --- /dev/null +++ b/faststream/_internal/endpoint/call_wrapper.py @@ -0,0 +1,205 @@ +import asyncio +from collections.abc import Awaitable, Callable, Mapping, Reversible, Sequence +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Optional, + Union, +) +from unittest.mock import MagicMock + +import anyio +from fast_depends import inject +from fast_depends.core import CallModel, build_call_model + +from faststream._internal.types import ( + MsgType, + P_HandlerParams, + T_HandlerReturn, +) +from faststream._internal.utils.functions import to_async +from faststream.exceptions import SetupError + +if TYPE_CHECKING: + from fast_depends.dependencies import Dependant + + from faststream._internal.basic_types import Decorator + from faststream._internal.di import FastDependsConfig + from faststream._internal.endpoint.publisher import PublisherProto + from faststream.message import StreamMessage + + +def ensure_call_wrapper( + call: Union[ + "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", + Callable[P_HandlerParams, T_HandlerReturn], + ], +) -> "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]": + if isinstance(call, HandlerCallWrapper): + return call + + return HandlerCallWrapper(call) + + +class HandlerCallWrapper(Generic[MsgType, P_HandlerParams, T_HandlerReturn]): + """A generic class to wrap handler calls.""" + + mock: MagicMock | None + future: Optional["asyncio.Future[Any]"] + is_test: bool + + _wrapped_call: Callable[..., Awaitable[Any]] | None + _original_call: Callable[P_HandlerParams, T_HandlerReturn] + _publishers: list["PublisherProto[MsgType]"] + + __slots__ = ( + "_original_call", + "_publishers", + "_wrapped_call", + "future", + "is_test", + "mock", + ) + + def __init__( + self, + call: Callable[P_HandlerParams, T_HandlerReturn], + ) -> None: + """Initialize a handler.""" + self._original_call = call + self._wrapped_call = None + self._publishers = [] + + self.mock = None + self.future = None + self.is_test = False + + def __call__( + self, + *args: P_HandlerParams.args, + **kwargs: P_HandlerParams.kwargs, + ) -> T_HandlerReturn: + """Calls the object as a function.""" + return self._original_call(*args, **kwargs) + + async def call_wrapped( + self, + message: "StreamMessage[MsgType]", + ) -> Any: + """Calls the wrapped function with the given message.""" + assert self._wrapped_call, "You should use `set_wrapped` first" # nosec B101 + if self.is_test: + assert self.mock # nosec B101 + self.mock(await message.decode()) + return await self._wrapped_call(message) + + async def wait_call(self, timeout: float | None = None) -> None: + """Waits for a call with an optional timeout.""" + assert ( # nosec B101 + self.future is not None + ), "You can use this method only with TestClient" + with anyio.fail_after(timeout): + await self.future + + def set_test(self) -> None: + self.is_test = True + if self.mock is None: + self.mock = MagicMock() + self.refresh(with_mock=True) + + def reset_test(self) -> None: + self.is_test = False + self.mock = None + self.future = None + + def trigger( + self, + result: Any = None, + error: BaseException | None = None, + ) -> None: + if not self.is_test: + return + + if self.future is None: + msg = "You can use this method only with TestClient" + raise SetupError(msg) + + if self.future.done(): + self.future = asyncio.Future() + + if error: + self.future.set_exception(error) + else: + self.future.set_result(result) + + def refresh(self, with_mock: bool = False) -> None: + if asyncio.events._get_running_loop() is not None: + self.future = asyncio.Future() + + if with_mock and self.mock is not None: + self.mock.reset_mock() + + def set_wrapped( + self, + *, + dependencies: Sequence["Dependant"], + _call_decorators: Reversible["Decorator"], + config: "FastDependsConfig", + ) -> Optional["CallModel"]: + call = self._original_call + for decor in reversed((*_call_decorators, *config.call_decorators)): + call = decor(call) + self._original_call = call + + f: Callable[..., Awaitable[Any]] = to_async(call) + + dependent: CallModel | None = None + if config.get_dependent is None: + assert config.provider + + dependent = build_call_model( + f, + extra_dependencies=dependencies, + dependency_provider=config.provider, + serializer_cls=config._serializer, + ) + + if config.use_fastdepends: + wrapper = inject( + func=None, + context__=config.context, + ) + f = wrapper(func=f, model=dependent) + + f = _wrap_decode_message( + func=f, + params_ln=len(dependent.flat_params), + ) + + self._wrapped_call = f + return dependent + + +def _wrap_decode_message( + func: Callable[..., Awaitable[T_HandlerReturn]], + params_ln: int, +) -> Callable[["StreamMessage[MsgType]"], Awaitable[T_HandlerReturn]]: + """Wraps a function to decode a message and pass it as an argument to the wrapped function.""" + + async def decode_wrapper(message: "StreamMessage[MsgType]") -> T_HandlerReturn: + """A wrapper function to decode and handle a message.""" + msg = await message.decode() + + if params_ln > 1: + if isinstance(msg, Mapping): + return await func(**msg) + if isinstance(msg, Sequence): + return await func(*msg) + else: + return await func(msg) + + msg = "unreachable" + raise AssertionError(msg) + + return decode_wrapper diff --git a/faststream/_internal/endpoint/publisher/__init__.py b/faststream/_internal/endpoint/publisher/__init__.py new file mode 100644 index 0000000000..0250784a46 --- /dev/null +++ b/faststream/_internal/endpoint/publisher/__init__.py @@ -0,0 +1,10 @@ +from .proto import BasePublisherProto, PublisherProto +from .specification import PublisherSpecification +from .usecase import PublisherUsecase + +__all__ = ( + "BasePublisherProto", + "PublisherProto", + "PublisherSpecification", + "PublisherUsecase", +) diff --git a/faststream/_internal/endpoint/publisher/fake.py b/faststream/_internal/endpoint/publisher/fake.py new file mode 100644 index 0000000000..7dd567ed10 --- /dev/null +++ b/faststream/_internal/endpoint/publisher/fake.py @@ -0,0 +1,73 @@ +from abc import abstractmethod +from collections.abc import Iterable +from functools import partial +from typing import TYPE_CHECKING, Any + +from faststream._internal.basic_types import SendableMessage +from faststream.response.publish_type import PublishType + +from .proto import BasePublisherProto + +if TYPE_CHECKING: + from faststream._internal.basic_types import AsyncFunc + from faststream._internal.producer import ProducerProto + from faststream._internal.types import PublisherMiddleware + from faststream.response.response import PublishCommand + + +class FakePublisher(BasePublisherProto): + """Publisher Interface implementation to use as RPC or REPLY TO answer publisher.""" + + def __init__( + self, + *, + producer: "ProducerProto", + ) -> None: + """Initialize an object.""" + self._producer = producer + + @abstractmethod + def patch_command(self, cmd: "PublishCommand") -> "PublishCommand": + cmd.publish_type = PublishType.REPLY + return cmd + + async def _publish( + self, + cmd: "PublishCommand", + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> Any: + """This method should be called in subscriber flow only.""" + cmd = self.patch_command(cmd) + + call: AsyncFunc = self._producer.publish + for m in _extra_middlewares: + call = partial(m, call) + + return await call(cmd) + + async def publish( + self, + message: SendableMessage, + /, + *, + correlation_id: str | None = None, + ) -> Any | None: + msg = ( + f"`{self.__class__.__name__}` can be used only to publish " + "a response for `reply-to` or `RPC` messages." + ) + raise NotImplementedError(msg) + + async def request( + self, + message: "SendableMessage", + /, + *, + correlation_id: str | None = None, + ) -> Any: + msg = ( + f"`{self.__class__.__name__}` can be used only to publish " + "a response for `reply-to` or `RPC` messages." + ) + raise NotImplementedError(msg) diff --git a/faststream/_internal/endpoint/publisher/proto.py b/faststream/_internal/endpoint/publisher/proto.py new file mode 100644 index 0000000000..dafc4fb984 --- /dev/null +++ b/faststream/_internal/endpoint/publisher/proto.py @@ -0,0 +1,65 @@ +from abc import abstractmethod +from collections.abc import Iterable, Sequence +from typing import ( + TYPE_CHECKING, + Any, + Protocol, +) + +from faststream._internal.endpoint.usecase import Endpoint +from faststream._internal.types import MsgType + +if TYPE_CHECKING: + from faststream._internal.basic_types import SendableMessage + from faststream._internal.types import PublisherMiddleware + from faststream.response import PublishCommand + + +class BasePublisherProto(Protocol): + @abstractmethod + async def publish( + self, + message: "SendableMessage", + /, + *, + correlation_id: str | None = None, + ) -> Any | None: + """Public method to publish a message. + + Should be called by user only `broker.publisher(...).publish(...)`. + """ + ... + + @abstractmethod + async def _publish( + self, + cmd: "PublishCommand", + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> None: + """Private method to publish a message. + + Should be called inside `publish` method or as a step of `consume` scope. + """ + ... + + @abstractmethod + async def request( + self, + message: "SendableMessage", + /, + *, + correlation_id: str | None = None, + ) -> Any | None: + """Publishes a message synchronously.""" + ... + + +class PublisherProto( + Endpoint[MsgType], + BasePublisherProto +): + _middlewares: Sequence["PublisherMiddleware"] + + @abstractmethod + async def start(self) -> None: ... diff --git a/faststream/_internal/endpoint/publisher/specification.py b/faststream/_internal/endpoint/publisher/specification.py new file mode 100644 index 0000000000..50e49ccc75 --- /dev/null +++ b/faststream/_internal/endpoint/publisher/specification.py @@ -0,0 +1,100 @@ +from inspect import Parameter, unwrap +from typing import ( + TYPE_CHECKING, + Generic, +) + +from fast_depends.core import build_call_model +from fast_depends.pydantic._compat import create_model, get_config_base +from typing_extensions import ( + TypeVar as TypeVar313, +) + +from faststream._internal.configs import ( + BrokerConfig, + PublisherSpecificationConfig, + SubscriberSpecificationConfig, +) +from faststream.specification.asyncapi.message import get_model_schema +from faststream.specification.asyncapi.utils import to_camelcase + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyCallable, AnyDict + from faststream.specification.schema import PublisherSpec + + +T_SpecificationConfig = TypeVar313( + "T_SpecificationConfig", bound=PublisherSpecificationConfig, default=SubscriberSpecificationConfig) +T_BrokerConfig = TypeVar313( + "T_BrokerConfig", bound=BrokerConfig, default=BrokerConfig) + + +class PublisherSpecification(Generic[T_BrokerConfig, T_SpecificationConfig]): + def __init__( + self, + _outer_config: "T_BrokerConfig", + specification_config: "T_SpecificationConfig", + ) -> None: + self.config = specification_config + self._outer_config = _outer_config + + self.calls: list[AnyCallable] = [] + + def add_call(self, call: "AnyCallable") -> None: + self.calls.append(call) + + @property + def include_in_schema(self) -> bool: + return self._outer_config.include_in_schema and self.config.include_in_schema + + def get_payloads(self) -> list[tuple["AnyDict", str]]: + payloads: list[tuple[AnyDict, str]] = [] + + if self.config.schema_: + body = get_model_schema( + call=create_model( + "", + __config__=get_config_base(), + response__=(self.config.schema_, ...), + ), + prefix=f"{self.name}:Message", + ) + + if body: # pragma: no branch + payloads.append((body, "")) + + else: + di_state = self._outer_config.fd_config + + for call in self.calls: + call_model = build_call_model( + call, + dependency_provider=di_state.provider, + serializer_cls=di_state._serializer, + ) + + response_type = next( + iter(call_model.serializer.response_option.values()) + ).field_type + + if response_type is not None and response_type is not Parameter.empty: + body = get_model_schema( + create_model( + "", + __config__=get_config_base(), + response__=(response_type, ...), + ), + prefix=f"{self.name}:Message", + ) + + if body: + payloads.append((body, to_camelcase(unwrap(call).__name__))) + + return payloads + + @property + def name(self) -> str: + raise NotImplementedError + + def get_schema(self) -> dict[str, "PublisherSpec"]: + raise NotImplementedError diff --git a/faststream/_internal/endpoint/publisher/usecase.py b/faststream/_internal/endpoint/publisher/usecase.py new file mode 100644 index 0000000000..b91dbcc3a5 --- /dev/null +++ b/faststream/_internal/endpoint/publisher/usecase.py @@ -0,0 +1,160 @@ +from collections.abc import Awaitable, Callable, Generator, Iterable +from functools import partial +from itertools import chain +from typing import ( + TYPE_CHECKING, + Any, +) +from unittest.mock import MagicMock + +from typing_extensions import override + +from faststream._internal.endpoint.call_wrapper import ( + HandlerCallWrapper, +) +from faststream._internal.endpoint.utils import process_msg +from faststream._internal.types import ( + MsgType, + P_HandlerParams, + T_HandlerReturn, +) +from faststream.message.source_type import SourceType + +from .proto import PublisherProto + +if TYPE_CHECKING: + from faststream._internal.configs import PublisherUsecaseConfig + from faststream._internal.producer import ProducerProto + from faststream._internal.types import ( + PublisherMiddleware, + ) + from faststream.response.response import PublishCommand + from faststream.specification.schema import PublisherSpec + + from .specification import PublisherSpecification + + +class PublisherUsecase(PublisherProto[MsgType]): + """A base class for publishers in an asynchronous API.""" + + def __init__( + self, + config: "PublisherUsecaseConfig", + specification: "PublisherSpecification", + ) -> None: + self.specification = specification + + broker_config = config._outer_config + self._outer_config = broker_config + + self.middlewares = config.middlewares + + self._fake_handler = False + self.mock: MagicMock | None = None + + @property + def include_in_schema(self) -> bool: + return self._outer_config.include_in_schema and self.include_in_schema_ + + @property + def _producer(self) -> "ProducerProto": + return self._outer_config.producer + + @override + async def start(self) -> None: + pass + + def set_test( + self, + *, + mock: MagicMock, + with_fake: bool, + ) -> None: + """Turn publisher to testing mode.""" + self.mock = mock + self._fake_handler = with_fake + + def reset_test(self) -> None: + """Turn off publisher's testing mode.""" + self._fake_handler = False + self.mock = None + + def __call__( + self, + func: Callable[P_HandlerParams, T_HandlerReturn] | HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn], + ) -> HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]: + """Decorate user's function by current publisher.""" + handler = super().__call__(func) + handler._publishers.append(self) + + self.specification.add_call(handler._original_call) + + return handler + + async def _basic_publish( + self, + cmd: "PublishCommand", + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> Any: + pub: Callable[..., Awaitable[Any]] = self._producer.publish + for pub_m in self._build_middlewares_stack(_extra_middlewares): + pub = partial(pub_m, pub) + + return await pub(cmd) + + async def _basic_publish_batch( + self, + cmd: "PublishCommand", + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> Any: + pub = self._producer.publish_batch + for pub_m in self._build_middlewares_stack(_extra_middlewares): + pub = partial(pub_m, pub) + + return await pub(cmd) + + async def _basic_request( + self, + cmd: "PublishCommand", + ) -> Any | None: + request = self._producer.request + for pub_m in self._build_middlewares_stack(): + request = partial(pub_m, request) + + published_msg = await request(cmd) + + context = self._outer_config.fd_config.context + + response_msg: Any = await process_msg( + msg=published_msg, + middlewares=( + m(published_msg, context=context) + for m in self._outer_config.broker_middlewares[::-1] + ), + parser=self._producer._parser, + decoder=self._producer._decoder, + source_type=SourceType.RESPONSE, + ) + return response_msg + + def _build_middlewares_stack( + self, + extra_middlewares: Iterable["PublisherMiddleware"] = (), + ) -> Generator["PublisherMiddleware", None, None]: + context = self._outer_config.fd_config.context + + yield from chain( + self.middlewares[::-1], + ( + extra_middlewares + or ( + m(None, context=context).publish_scope + for m in self._outer_config.broker_middlewares[::-1] + ) + ), + ) + + def schema(self) -> dict[str, "PublisherSpec"]: + return self.specification.get_schema() diff --git a/faststream/_internal/endpoint/subscriber/__init__.py b/faststream/_internal/endpoint/subscriber/__init__.py new file mode 100644 index 0000000000..45e293b5e7 --- /dev/null +++ b/faststream/_internal/endpoint/subscriber/__init__.py @@ -0,0 +1,11 @@ +from .specification import SubscriberSpecification +from .usecase import ( + SubscriberProto, + SubscriberUsecase, +) + +__all__ = ( + "SubscriberProto", + "SubscriberSpecification", + "SubscriberUsecase", +) diff --git a/faststream/_internal/endpoint/subscriber/call_item.py b/faststream/_internal/endpoint/subscriber/call_item.py new file mode 100644 index 0000000000..8488b8ae55 --- /dev/null +++ b/faststream/_internal/endpoint/subscriber/call_item.py @@ -0,0 +1,197 @@ +from collections import UserList +from collections.abc import Iterable, Reversible, Sequence +from functools import partial +from inspect import unwrap +from itertools import chain +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Optional, + cast, +) + +from faststream._internal.types import MsgType +from faststream.exceptions import IgnoredException, SetupError +from faststream.specification.asyncapi.utils import to_camelcase + +if TYPE_CHECKING: + from fast_depends.dependencies import Dependant + + from faststream._internal.basic_types import AsyncFuncAny, Decorator + from faststream._internal.di import FastDependsConfig + from faststream._internal.endpoint.call_wrapper import HandlerCallWrapper + from faststream._internal.types import ( + AsyncCallable, + AsyncFilter, + CustomCallable, + SubscriberMiddleware, + ) + from faststream.message import StreamMessage + + +class HandlerItem(Generic[MsgType]): + """A class representing handler overloaded item.""" + + __slots__ = ( + "dependant", + "dependencies", + "filter", + "handler", + "item_decoder", + "item_middlewares", + "item_parser", + ) + + dependant: Any | None + + def __init__( + self, + *, + handler: "HandlerCallWrapper[MsgType, ..., Any]", + filter: "AsyncFilter[StreamMessage[MsgType]]", + item_parser: Optional["CustomCallable"], + item_decoder: Optional["CustomCallable"], + item_middlewares: Sequence["SubscriberMiddleware[StreamMessage[MsgType]]"], + dependencies: Iterable["Dependant"], + ) -> None: + self.handler = handler + self.filter = filter + self.item_parser = item_parser + self.item_decoder = item_decoder + self.item_middlewares = item_middlewares + self.dependencies = dependencies + self.dependant = None + + def __repr__(self) -> str: + filter_call = unwrap(self.filter) + filter_name = getattr(filter_call, "__name__", str(filter_call)) + return f"<'{self.name}': filter='{filter_name}'>" + + def _setup( + self, + *, + parser: "AsyncCallable", + decoder: "AsyncCallable", + config: "FastDependsConfig", + broker_dependencies: Iterable["Dependant"], + _call_decorators: Reversible["Decorator"], + ) -> None: + if self.dependant is None: + self.item_parser = parser + self.item_decoder = decoder + + dependencies = (*broker_dependencies, *self.dependencies) + + dependant = self.handler.set_wrapped( + dependencies=dependencies, + _call_decorators=_call_decorators, + config=config, + ) + + if config.get_dependent is None: + self.dependant = dependant + + else: + self.dependant = config.get_dependent( + self.handler._original_call, + dependencies, + ) + + @property + def name(self) -> str: + """Returns the name of the original call.""" + if self.handler is None: + return "" + + caller = unwrap(self.handler._original_call) + return getattr(caller, "__name__", str(caller)) + + @property + def description(self) -> str | None: + """Returns the description of original call.""" + if self.handler is None: + return None + + caller = unwrap(self.handler._original_call) + return getattr(caller, "__doc__", None) + + async def is_suitable( + self, + msg: MsgType, + cache: dict[Any, Any], + ) -> Optional["StreamMessage[MsgType]"]: + """Check is message suite for current filter.""" + if not (parser := cast("AsyncCallable | None", self.item_parser)) or not ( + decoder := cast("AsyncCallable | None", self.item_decoder) + ): + error_msg = "You should setup `HandlerItem` at first." + raise SetupError(error_msg) + + message = cache[parser] = cast( + "StreamMessage[MsgType]", + cache.get(parser) or await parser(msg), + ) + + # NOTE: final decoder will be set for success filter + message.set_decoder(decoder) + + if await self.filter(message): + return message + + return None + + async def call( + self, + /, + message: "StreamMessage[MsgType]", + _extra_middlewares: Iterable["SubscriberMiddleware[Any]"], + ) -> Any: + """Execute wrapped handler with consume middlewares.""" + call: AsyncFuncAny = self.handler.call_wrapped + + for middleware in chain(self.item_middlewares[::-1], _extra_middlewares): + call = partial(middleware, call) + + try: + result = await call(message) + + except (IgnoredException, SystemExit): + self.handler.trigger() + raise + + except Exception as e: + self.handler.trigger(error=e) + raise + + else: + self.handler.trigger(result=result) + return result + + +class CallsCollection(UserList[HandlerItem[MsgType]]): + def add_call(self, call: "HandlerItem[MsgType]") -> None: + self.data.append(call) + + @property + def name(self) -> str | None: + """Returns the name of the handler call.""" + if not self.data: + return None + + if len(self.data) == 1: + return to_camelcase(self.data[0].name) + + return f"[{','.join(to_camelcase(c.name) for c in self.data)}]" + + @property + def description(self) -> str | None: + if not self.data: + return None + + if len(self.data) == 1: + return self.data[0].description + + return "\n".join( + f"{to_camelcase(h.name)}: {h.description or ''}" for h in self.data + ) diff --git a/faststream/broker/subscriber/mixins.py b/faststream/_internal/endpoint/subscriber/mixins.py similarity index 87% rename from faststream/broker/subscriber/mixins.py rename to faststream/_internal/endpoint/subscriber/mixins.py index 2043d8b1ae..19d7d9a5d4 100644 --- a/faststream/broker/subscriber/mixins.py +++ b/faststream/_internal/endpoint/subscriber/mixins.py @@ -1,15 +1,10 @@ import asyncio -from typing import ( - TYPE_CHECKING, - Any, - Coroutine, - Generic, - List, -) +from collections.abc import Coroutine +from typing import TYPE_CHECKING, Any, Generic import anyio -from faststream.broker.types import MsgType +from faststream._internal.types import MsgType from .usecase import SubscriberUsecase @@ -20,7 +15,7 @@ class TasksMixin(SubscriberUsecase[Any]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.tasks: List[asyncio.Task[Any]] = [] + self.tasks: list[asyncio.Task[Any]] = [] def add_task(self, coro: Coroutine[Any, Any, Any]) -> None: self.tasks.append(asyncio.create_task(coro)) @@ -33,7 +28,7 @@ async def close(self) -> None: if not task.done(): task.cancel() - self.tasks = [] + self.tasks.clear() class ConcurrentMixin(TasksMixin, Generic[MsgType]): @@ -69,10 +64,7 @@ async def _serve_consume_queue( async for msg in self.receive_stream: tg.start_soon(self._consume_msg, msg) - async def _consume_msg( - self, - msg: "MsgType", - ) -> None: + async def _consume_msg(self, msg: "MsgType") -> None: """Proxy method to call `self.consume` with semaphore block.""" async with self.limiter: await self.consume(msg) diff --git a/faststream/_internal/endpoint/subscriber/proto.py b/faststream/_internal/endpoint/subscriber/proto.py new file mode 100644 index 0000000000..e5cc4596fb --- /dev/null +++ b/faststream/_internal/endpoint/subscriber/proto.py @@ -0,0 +1,66 @@ +from abc import abstractmethod +from collections.abc import AsyncIterator, Iterable, Sequence +from typing import TYPE_CHECKING, Any, Optional + +from typing_extensions import Self + +from faststream._internal.endpoint.usecase import Endpoint +from faststream._internal.types import MsgType + +if TYPE_CHECKING: + from fast_depends.dependencies import Dependant + + from faststream._internal.endpoint.publisher import BasePublisherProto + from faststream._internal.endpoint.subscriber.call_item import CallsCollection + from faststream._internal.types import CustomCallable, SubscriberMiddleware + from faststream.message import StreamMessage + from faststream.response import Response + + +class SubscriberProto(Endpoint[MsgType]): + calls: "CallsCollection[MsgType]" + running: bool + + async def start(self) -> None: ... + + @abstractmethod + def get_log_context( + self, + msg: Optional["StreamMessage[MsgType]"], + /, + ) -> dict[str, str]: ... + + @abstractmethod + def _make_response_publisher( + self, + message: "StreamMessage[MsgType]", + ) -> Iterable["BasePublisherProto"]: ... + + @abstractmethod + async def close(self) -> None: ... + + @abstractmethod + async def consume(self, msg: MsgType) -> Any: ... + + @abstractmethod + async def process_message(self, msg: MsgType) -> "Response": ... + + @abstractmethod + async def get_one( + self, + *, + timeout: float = 5.0, + ) -> "StreamMessage[MsgType] | None": ... + + @abstractmethod + def add_call( + self, + *, + parser_: "CustomCallable", + decoder_: "CustomCallable", + middlewares_: Sequence["SubscriberMiddleware[Any]"], + dependencies_: Iterable["Dependant"], + ) -> Self: ... + + @abstractmethod + def __aiter__(self) -> AsyncIterator["StreamMessage[MsgType]"]: ... diff --git a/faststream/_internal/endpoint/subscriber/specification.py b/faststream/_internal/endpoint/subscriber/specification.py new file mode 100644 index 0000000000..5b798f6f61 --- /dev/null +++ b/faststream/_internal/endpoint/subscriber/specification.py @@ -0,0 +1,86 @@ +from abc import abstractmethod +from typing import ( + TYPE_CHECKING, + Any, + Generic, +) + +from typing_extensions import ( + TypeVar as TypeVar313, +) + +from faststream._internal.configs import BrokerConfig, SubscriberSpecificationConfig +from faststream.exceptions import SetupError +from faststream.specification.asyncapi.message import parse_handler_params +from faststream.specification.asyncapi.utils import to_camelcase + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict + from faststream._internal.endpoint.subscriber.call_item import ( + CallsCollection, + ) + from faststream.specification.schema import SubscriberSpec + + +T_SpecificationConfig = TypeVar313( + "T_SpecificationConfig", bound=SubscriberSpecificationConfig, default=SubscriberSpecificationConfig) +T_BrokerConfig = TypeVar313( + "T_BrokerConfig", bound=BrokerConfig, default=BrokerConfig) + + +class SubscriberSpecification(Generic[T_BrokerConfig, T_SpecificationConfig]): + def __init__( + self, + _outer_config: "T_BrokerConfig", + specification_config: "T_SpecificationConfig", + calls: "CallsCollection[Any]", + ) -> None: + self.calls = calls + self.config = specification_config + self._outer_config = _outer_config + + @property + def include_in_schema(self) -> bool: + return self._outer_config.include_in_schema and self.config.include_in_schema + + @property + def description(self) -> str | None: + return self.config.description_ or self.calls.description + + @property + def call_name(self) -> str: + return self.calls.name or "Subscriber" + + def get_payloads(self) -> list[tuple["AnyDict", str]]: + payloads: list[tuple[AnyDict, str]] = [] + + call_name = self.call_name + + for h in self.calls: + if h.dependant is None: + msg = "You should setup `Handler` at first." + raise SetupError(msg) + + body = parse_handler_params(h.dependant, prefix=f"{self.config.title_ or call_name}:Message") + payloads.append((body, to_camelcase(h.name))) + + if not self.calls: + payloads.append( + ( + { + "title": f"{self.config.title_ or call_name}:Message:Payload", + }, + to_camelcase(call_name), + ), + ) + + return payloads + + @property + @abstractmethod + def name(self) -> str: + raise NotImplementedError + + @abstractmethod + def get_schema(self) -> dict[str, "SubscriberSpec"]: + raise NotImplementedError diff --git a/faststream/_internal/endpoint/subscriber/usecase.py b/faststream/_internal/endpoint/subscriber/usecase.py new file mode 100644 index 0000000000..7fefadf8b3 --- /dev/null +++ b/faststream/_internal/endpoint/subscriber/usecase.py @@ -0,0 +1,430 @@ +from abc import abstractmethod +from collections.abc import Callable, Iterable, Sequence +from contextlib import AbstractContextManager, AsyncExitStack +from itertools import chain +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + NamedTuple, + Optional, + Union, +) + +from typing_extensions import Self, deprecated, overload, override + +from faststream._internal.endpoint.utils import resolve_custom_func +from faststream._internal.types import ( + MsgType, + P_HandlerParams, + T_HandlerReturn, +) +from faststream._internal.utils.functions import FakeContext, to_async +from faststream.exceptions import SetupError, StopConsume, SubscriberNotFound +from faststream.middlewares import AckPolicy, AcknowledgementMiddleware +from faststream.middlewares.logging import CriticalLogMiddleware +from faststream.response import ensure_response + +from .call_item import ( + CallsCollection, + HandlerItem, +) +from .proto import SubscriberProto +from .utils import MultiLock, default_filter + +if TYPE_CHECKING: + from fast_depends.dependencies import Dependant + + from faststream._internal.basic_types import AnyDict + from faststream._internal.configs import SubscriberUsecaseConfig + from faststream._internal.endpoint.call_wrapper import HandlerCallWrapper + from faststream._internal.endpoint.publisher import BasePublisherProto + from faststream._internal.types import ( + AsyncFilter, + BrokerMiddleware, + CustomCallable, + Filter, + SubscriberMiddleware, + ) + from faststream.message import StreamMessage + from faststream.middlewares import BaseMiddleware + from faststream.response import Response + from faststream.specification.schema import SubscriberSpec + + from .specification import SubscriberSpecification + + +class _CallOptions(NamedTuple): + parser: Optional["CustomCallable"] + decoder: Optional["CustomCallable"] + middlewares: Sequence["SubscriberMiddleware[Any]"] + dependencies: Iterable["Dependant"] + + +class SubscriberUsecase(SubscriberProto[MsgType]): + """A class representing an asynchronous handler.""" + + lock: "AbstractContextManager[Any]" + extra_watcher_options: "AnyDict" + graceful_timeout: float | None + + _call_options: Optional["_CallOptions"] + + def __init__( + self, + config: "SubscriberUsecaseConfig", + specification: "SubscriberSpecification", + calls: "CallsCollection", + ) -> None: + """Initialize a new instance of the class.""" + self.calls = calls + self.specification = specification + + self._no_reply = config.no_reply + self._parser = config.parser + self._decoder = config.decoder + self.ack_policy = config.ack_policy + + self._call_options = None + self._call_decorators = () + + self.running = False + self.lock = FakeContext() + + # Setup in registration + self._outer_config = config._outer_config + + self.extra_watcher_options = {} + + @property + def _broker_middlewares(self) -> Sequence["BrokerMiddleware[MsgType]"]: + return self._outer_config.broker_middlewares + + async def start(self) -> None: + """Private method to start subscriber by broker.""" + self.lock = MultiLock() + + self._build_fastdepends_model() + + self._outer_config.logger.log( + f"`{self.specification.call_name}` waiting for messages", + extra=self.get_log_context(None), + ) + + def _build_fastdepends_model(self) -> None: + for call in self.calls: + if parser := call.item_parser or self._outer_config.broker_parser: + async_parser = resolve_custom_func(parser, self._parser) + else: + async_parser = self._parser + + if decoder := call.item_decoder or self._outer_config.broker_decoder: + async_decoder = resolve_custom_func(decoder, self._decoder) + else: + async_decoder = self._decoder + + call._setup( + parser=async_parser, + decoder=async_decoder, + config=self._outer_config.fd_config, + broker_dependencies=self._outer_config.broker_dependencies, + _call_decorators=self._call_decorators, + ) + + call.handler.refresh(with_mock=False) + + def _post_start(self) -> None: + self.running = True + + @abstractmethod + async def close(self) -> None: + """Close the handler. + + Blocks event loop up to graceful_timeout seconds. + """ + self.running = False + if isinstance(self.lock, MultiLock): + await self.lock.wait_release(self._outer_config.graceful_timeout) + + def add_call( + self, + *, + parser_: Optional["CustomCallable"], + decoder_: Optional["CustomCallable"], + middlewares_: Sequence["SubscriberMiddleware[Any]"], + dependencies_: Iterable["Dependant"], + ) -> Self: + self._call_options = _CallOptions( + parser=parser_, + decoder=decoder_, + middlewares=middlewares_, + dependencies=dependencies_, + ) + return self + + @overload + def __call__( + self, + func: None = None, + *, + filter: "Filter[StreamMessage[MsgType]]" = default_filter, + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, + middlewares: Annotated[ + Sequence["SubscriberMiddleware[Any]"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + ] = (), + dependencies: Iterable["Dependant"] = (), + ) -> Callable[ + [Callable[P_HandlerParams, T_HandlerReturn]], + "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", + ]: ... + + @overload + def __call__( + self, + func: Union[ + Callable[P_HandlerParams, T_HandlerReturn], + "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", + ], + *, + filter: "Filter[StreamMessage[MsgType]]" = default_filter, + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, + middlewares: Annotated[ + Sequence["SubscriberMiddleware[Any]"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + ] = (), + dependencies: Iterable["Dependant"] = (), + ) -> "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]": ... + + @override + def __call__( + self, + func: Union[ + Callable[P_HandlerParams, T_HandlerReturn], + "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", + None, + ] = None, + *, + filter: "Filter[StreamMessage[MsgType]]" = default_filter, + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, + middlewares: Annotated[ + Sequence["SubscriberMiddleware[Any]"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + ] = (), + dependencies: Iterable["Dependant"] = (), + ) -> Union[ + "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", + Callable[ + [Callable[P_HandlerParams, T_HandlerReturn]], + "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", + ], + ]: + if (options := self._call_options) is None: + msg = ( + "You can't create subscriber directly. Please, use `add_call` at first." + ) + raise SetupError(msg) + + total_deps = (*options.dependencies, *dependencies) + total_middlewares = (*options.middlewares, *middlewares) + async_filter: AsyncFilter[StreamMessage[MsgType]] = to_async(filter) + + def real_wrapper( + func: Union[ + Callable[P_HandlerParams, T_HandlerReturn], + "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", + ], + ) -> "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]": + handler = super(SubscriberUsecase, self).__call__(func) + + self.calls.add_call( + HandlerItem[MsgType]( + handler=handler, + filter=async_filter, + item_parser=parser or options.parser, + item_decoder=decoder or options.decoder, + item_middlewares=total_middlewares, + dependencies=total_deps, + ), + ) + + return handler + + if func is None: + return real_wrapper + + return real_wrapper(func) + + async def consume(self, msg: MsgType) -> Any: + """Consume a message asynchronously.""" + if not self.running: + return None + + try: + return await self.process_message(msg) + + except StopConsume: + # Stop handler at StopConsume exception + await self.close() + + except SystemExit: + # Stop handler at `exit()` call + await self.close() + + if app := self._outer_config.fd_config.context.get("app"): + app.exit() + + except Exception: # nosec B110 + # All other exceptions were logged by CriticalLogMiddleware + pass + + async def process_message(self, msg: MsgType) -> "Response": + """Execute all message processing stages.""" + context = self._outer_config.fd_config.context + logger_state = self._outer_config.logger + + async with AsyncExitStack() as stack: + stack.enter_context(self.lock) + + # Enter context before middlewares + stack.enter_context(context.scope("logger", logger_state.logger.logger)) + for k, v in self._outer_config.extra_context.items(): + stack.enter_context(context.scope(k, v)) + + # enter all middlewares + middlewares: list[BaseMiddleware] = [] + for base_m in self.__build__middlewares_stack(): + middleware = base_m(msg, context=context) + middlewares.append(middleware) + await middleware.__aenter__() + + cache: dict[Any, Any] = {} + parsing_error: Exception | None = None + for h in self.calls: + try: + message = await h.is_suitable(msg, cache) + except Exception as e: + parsing_error = e + break + + if message is not None: + stack.enter_context( + context.scope("log_context", self.get_log_context(message)) + ) + stack.enter_context(context.scope("message", message)) + + # Middlewares should be exited before scope release + for m in middlewares: + stack.push_async_exit(m.__aexit__) + + result_msg = ensure_response( + await h.call( + message=message, + # consumer middlewares + _extra_middlewares=( + m.consume_scope for m in middlewares[::-1] + ), + ), + ) + + if not result_msg.correlation_id: + result_msg.correlation_id = message.correlation_id + + for p in chain( + self.__get_response_publisher(message), + h.handler._publishers, + ): + await p._publish( + result_msg.as_publish_command(), + _extra_middlewares=( + m.publish_scope for m in middlewares[::-1] + ), + ) + + # Return data for tests + return result_msg + + # Suitable handler was not found or + # parsing/decoding exception occurred + for m in middlewares: + stack.push_async_exit(m.__aexit__) + + # Reraise it to catch in tests + if parsing_error: + raise parsing_error + + error_msg = f"There is no suitable handler for {msg=}" + raise SubscriberNotFound(error_msg) + + # An error was raised and processed by some middleware + return ensure_response(None) + + def __build__middlewares_stack(self) -> tuple["BrokerMiddleware[MsgType]", ...]: + logger_state = self._outer_config.logger + + if self.ack_policy is AckPolicy.DO_NOTHING: + broker_middlewares = ( + CriticalLogMiddleware(logger_state), + *self._broker_middlewares, + ) + + else: + broker_middlewares = ( + AcknowledgementMiddleware( + logger=logger_state, + ack_policy=self.ack_policy, + extra_options=self.extra_watcher_options, + ), + CriticalLogMiddleware(logger_state), + *self._broker_middlewares, + ) + + return broker_middlewares + + def __get_response_publisher( + self, + message: "StreamMessage[MsgType]", + ) -> Iterable["BasePublisherProto"]: + if not message.reply_to or self._no_reply: + return () + + return self._make_response_publisher(message) + + def get_log_context( + self, + message: Optional["StreamMessage[MsgType]"], + ) -> dict[str, str]: + """Generate log context.""" + return { + "message_id": getattr(message, "message_id", ""), + } + + def _log( + self, + log_level: int | None, + message: str, + extra: Optional["AnyDict"] = None, + exc_info: Exception | None = None, + ) -> None: + self._outer_config.logger.log( + message, + log_level, + extra=extra, + exc_info=exc_info, + ) + + def schema(self) -> dict[str, "SubscriberSpec"]: + self._build_fastdepends_model() + return self.specification.get_schema() diff --git a/faststream/_internal/endpoint/subscriber/utils.py b/faststream/_internal/endpoint/subscriber/utils.py new file mode 100644 index 0000000000..68f73136eb --- /dev/null +++ b/faststream/_internal/endpoint/subscriber/utils.py @@ -0,0 +1,75 @@ +import asyncio +from contextlib import suppress +from typing import ( + TYPE_CHECKING, + Any, + Optional, +) + +import anyio +from typing_extensions import Self + +if TYPE_CHECKING: + from types import TracebackType + + from faststream.message import StreamMessage + + +async def default_filter(msg: "StreamMessage[Any]") -> bool: + """A function to filter stream messages.""" + return not msg.processed + + +class MultiLock: + """A class representing a multi lock. + + This lock can be acquired multiple times. + `wait_release` method waits for all locks will be released. + """ + + def __init__(self) -> None: + """Initialize a new instance of the class.""" + self.queue: asyncio.Queue[None] = asyncio.Queue() + + def __enter__(self) -> Self: + """Enter the context.""" + self.acquire() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: Optional["TracebackType"], + ) -> None: + """Exit the context.""" + self.release() + + def acquire(self) -> None: + """Acquire lock.""" + self.queue.put_nowait(None) + + def release(self) -> None: + """Release lock.""" + with suppress(asyncio.QueueEmpty, ValueError): + self.queue.get_nowait() + self.queue.task_done() + + @property + def qsize(self) -> int: + """Return the size of the queue.""" + return self.queue.qsize() + + @property + def empty(self) -> bool: + """Return whether the queue is empty.""" + return self.queue.empty() + + async def wait_release(self, timeout: float | None = None) -> None: + """Wait for the queue to be released. + + Using for graceful shutdown. + """ + if timeout: + with anyio.move_on_after(timeout): + await self.queue.join() diff --git a/faststream/_internal/endpoint/usecase.py b/faststream/_internal/endpoint/usecase.py new file mode 100644 index 0000000000..08997f91bf --- /dev/null +++ b/faststream/_internal/endpoint/usecase.py @@ -0,0 +1,36 @@ +from collections.abc import Callable, Sequence +from typing import TYPE_CHECKING, Protocol + +from faststream._internal.types import ( + BrokerMiddleware, + MsgType, + P_HandlerParams, + T_HandlerReturn, +) + +from .call_wrapper import ( + HandlerCallWrapper, + ensure_call_wrapper, +) + +if TYPE_CHECKING: + from faststream._internal.producer import ProducerProto + + +class Endpoint(Protocol[MsgType]): + @property + def _producer(self) -> "ProducerProto": + raise NotImplementedError + + @property + def _broker_middlewares(self) -> Sequence["BrokerMiddleware[MsgType]"]: + raise NotImplementedError + + def __call__( + self, + func: Callable[P_HandlerParams, T_HandlerReturn] | HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn], + ) -> HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]: + handler: HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn] = ( + ensure_call_wrapper(func) + ) + return handler diff --git a/faststream/_internal/endpoint/utils.py b/faststream/_internal/endpoint/utils.py new file mode 100644 index 0000000000..1c90624d94 --- /dev/null +++ b/faststream/_internal/endpoint/utils.py @@ -0,0 +1,70 @@ +import inspect +from collections.abc import Awaitable, Callable, Iterable +from contextlib import AsyncExitStack +from functools import partial +from typing import ( + TYPE_CHECKING, + Any, + Optional, + cast, +) + +from faststream._internal.types import MsgType +from faststream._internal.utils.functions import return_input, to_async +from faststream.message.source_type import SourceType + +if TYPE_CHECKING: + from faststream._internal.types import ( + AsyncCallable, + CustomCallable, + SyncCallable, + ) + from faststream.message import StreamMessage + from faststream.middlewares import BaseMiddleware + + +async def process_msg( + msg: MsgType | None, + *, + middlewares: Iterable["BaseMiddleware"], + parser: Callable[[MsgType], Awaitable["StreamMessage[MsgType]"]], + decoder: Callable[["StreamMessage[MsgType]"], "Any"], + source_type: SourceType = SourceType.CONSUME, +) -> Optional["StreamMessage[MsgType]"]: + if msg is None: + return None + + async with AsyncExitStack() as stack: + return_msg: Callable[ + [StreamMessage[MsgType]], + Awaitable[StreamMessage[MsgType]], + ] = return_input + + for m in middlewares: + await stack.enter_async_context(m) + return_msg = partial(m.consume_scope, return_msg) + + parsed_msg = await parser(msg) + parsed_msg._source_type = source_type + parsed_msg.set_decoder(decoder) + return await return_msg(parsed_msg) + + error_msg = "unreachable" + raise AssertionError(error_msg) + + +def resolve_custom_func( + custom_func: Optional["CustomCallable"], + default_func: "AsyncCallable", +) -> "AsyncCallable": + """Resolve a custom parser/decoder with default one.""" + if custom_func is None: + return default_func + + original_params = inspect.signature(custom_func).parameters + + if len(original_params) == 1: + return to_async(cast("SyncCallable | AsyncCallable", custom_func)) + + name = tuple(original_params.items())[1][0] + return partial(to_async(custom_func), **{name: default_func}) diff --git a/faststream/_internal/fastapi/__init__.py b/faststream/_internal/fastapi/__init__.py new file mode 100644 index 0000000000..011777a593 --- /dev/null +++ b/faststream/_internal/fastapi/__init__.py @@ -0,0 +1,7 @@ +from faststream._internal.fastapi.route import StreamMessage +from faststream._internal.fastapi.router import StreamRouter + +__all__ = ( + "StreamMessage", + "StreamRouter", +) diff --git a/faststream/_internal/fastapi/_compat.py b/faststream/_internal/fastapi/_compat.py new file mode 100644 index 0000000000..6d28a34eb3 --- /dev/null +++ b/faststream/_internal/fastapi/_compat.py @@ -0,0 +1,137 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from fastapi import __version__ as FASTAPI_VERSION # noqa: N812 +from fastapi.dependencies.utils import solve_dependencies +from starlette.background import BackgroundTasks +from typing_extensions import Never + +from faststream._internal.basic_types import AnyDict + +if TYPE_CHECKING: + from fastapi.dependencies.models import Dependant + from fastapi.requests import Request + +major, minor, patch, *_ = FASTAPI_VERSION.split(".") + +_FASTAPI_MAJOR, _FASTAPI_MINOR = int(major), int(minor) + +FASTAPI_V2 = _FASTAPI_MAJOR > 0 or _FASTAPI_MINOR > 100 +FASTAPI_V106 = _FASTAPI_MAJOR > 0 or _FASTAPI_MINOR >= 106 + +try: + _FASTAPI_PATCH = int(patch) +except ValueError: + FASTAPI_v102_3 = True + FASTAPI_v102_4 = True +else: + FASTAPI_v102_3 = ( + _FASTAPI_MAJOR > 0 + or _FASTAPI_MINOR > 112 + or (_FASTAPI_MINOR == 112 and _FASTAPI_PATCH > 2) + ) + FASTAPI_v102_4 = ( + _FASTAPI_MAJOR > 0 + or _FASTAPI_MINOR > 112 + or (_FASTAPI_MINOR == 112 and _FASTAPI_PATCH > 3) + ) + +__all__ = ( + "RequestValidationError", + "create_response_field", + "raise_fastapi_validation_error", + "solve_faststream_dependency", +) + + +@dataclass +class SolvedDependency: + values: AnyDict + errors: list[Any] + background_tasks: BackgroundTasks | None + + +if FASTAPI_V2: + from fastapi._compat import _normalize_errors + from fastapi.exceptions import RequestValidationError + + def raise_fastapi_validation_error(errors: list[Any], body: AnyDict) -> Never: + raise RequestValidationError(_normalize_errors(errors), body=body) + +else: + from pydantic import ( # type: ignore[assignment] + ValidationError as RequestValidationError, + create_model, + ) + + ROUTER_VALIDATION_ERROR_MODEL = create_model("StreamRoute") + + def raise_fastapi_validation_error(errors: list[Any], body: AnyDict) -> Never: + raise RequestValidationError(errors, ROUTER_VALIDATION_ERROR_MODEL) # type: ignore[misc] + + +if FASTAPI_v102_3: + from fastapi.utils import ( + create_model_field as create_response_field, + ) + + extra = {"embed_body_fields": False} if FASTAPI_v102_4 else {} + + async def solve_faststream_dependency( + request: "Request", + dependant: "Dependant", + dependency_overrides_provider: Any | None, + **kwargs: Any, + ) -> SolvedDependency: + solved_result = await solve_dependencies( + request=request, + body=request._body, # type: ignore[arg-type] + dependant=dependant, + dependency_overrides_provider=dependency_overrides_provider, + **extra, # type: ignore[arg-type] + **kwargs, + ) + values, errors, background = ( + solved_result.values, + solved_result.errors, + solved_result.background_tasks, + ) + + return SolvedDependency( + values=values, + errors=errors, + background_tasks=background, + ) + +else: + from fastapi.utils import ( # type: ignore[attr-defined,no-redef] + create_response_field, + ) + + async def solve_faststream_dependency( + request: "Request", + dependant: "Dependant", + dependency_overrides_provider: Any | None, + **kwargs: Any, + ) -> SolvedDependency: + solved_result = await solve_dependencies( + request=request, + body=request._body, # type: ignore[arg-type] + dependant=dependant, + dependency_overrides_provider=dependency_overrides_provider, + **kwargs, + ) + + ( + values, + errors, + background, + _response, + _dependency_cache, + ) = solved_result # type: ignore[misc] + + return SolvedDependency( + values=values, # type: ignore[has-type] + errors=errors, # type: ignore[has-type] + background_tasks=background, # type: ignore[has-type] + ) diff --git a/faststream/_internal/fastapi/config.py b/faststream/_internal/fastapi/config.py new file mode 100644 index 0000000000..0730e211bf --- /dev/null +++ b/faststream/_internal/fastapi/config.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Optional + +if TYPE_CHECKING: + from fastapi import FastAPI + + +@dataclass +class FastAPIConfig: + dependency_overrides_provider: Any | None + application: Optional["FastAPI"] = None + + def set_application(self, app: "FastAPI") -> None: + self.application = app + self.dependency_overrides_provider = self.dependency_overrides_provider or app diff --git a/faststream/_internal/fastapi/context.py b/faststream/_internal/fastapi/context.py new file mode 100644 index 0000000000..e5ef5a017d --- /dev/null +++ b/faststream/_internal/fastapi/context.py @@ -0,0 +1,34 @@ +import logging +from collections.abc import Callable +from typing import Annotated, Any + +from fastapi import params + +from faststream._internal.constants import EMPTY +from faststream._internal.context import ContextRepo as CR +from faststream._internal.context.resolve import resolve_context_by_name + + +def Context( # noqa: N802 + name: str, + *, + default: Any = EMPTY, + initial: Callable[..., Any] | None = None, +) -> Any: + """Get access to objects of the Context.""" + + def solve_context( + context: Annotated[Any, params.Header(alias="context__")], + ) -> Any: + return resolve_context_by_name( + name=name, + default=default, + initial=initial, + context=context, + ) + + return params.Depends(solve_context, use_cache=True) + + +Logger = Annotated[logging.Logger, Context("logger")] +ContextRepo = Annotated[CR, Context("context")] diff --git a/faststream/broker/fastapi/get_dependant.py b/faststream/_internal/fastapi/get_dependant.py similarity index 87% rename from faststream/broker/fastapi/get_dependant.py rename to faststream/_internal/fastapi/get_dependant.py index 468d74f4bb..14f6c53748 100644 --- a/faststream/broker/fastapi/get_dependant.py +++ b/faststream/_internal/fastapi/get_dependant.py @@ -1,6 +1,8 @@ import inspect -from typing import TYPE_CHECKING, Any, Callable, Iterable, Set, Tuple, cast +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, Annotated, Any, cast, get_args, get_origin +from fast_depends.library.serializer import OptionItem from fast_depends.utils import get_typed_annotation from fastapi import params from fastapi.dependencies.utils import ( @@ -8,12 +10,11 @@ get_parameterless_sub_dependant, get_typed_signature, ) -from typing_extensions import Annotated, get_args, get_origin -from faststream._compat import PYDANTIC_V2 +from faststream._internal._compat import PYDANTIC_V2 if TYPE_CHECKING: - from fastapi.dependencies.models import Dependant + from fastapi.dependencies import models def get_fastapi_dependant( @@ -26,9 +27,7 @@ def get_fastapi_dependant( dependencies=dependencies, ) - dependent = _patch_fastapi_dependent(dependent) - - return dependent + return _patch_fastapi_dependent(dependent) def get_fastapi_native_dependant( @@ -50,11 +49,11 @@ def get_fastapi_native_dependant( return dependent -def _patch_fastapi_dependent(dependant: "Dependant") -> "Dependant": +def _patch_fastapi_dependent(dependant: "models.Dependant") -> "models.Dependant": """Patch FastAPI by adding fields for AsyncAPI schema generation.""" from pydantic import Field, create_model # FastAPI always has pydantic - from faststream._compat import PydanticUndefined + from faststream._internal._compat import PydanticUndefined params = dependant.query_params + dependant.body_params @@ -94,7 +93,7 @@ def _patch_fastapi_dependent(dependant: "Dependant") -> "Dependant": "examples": info.examples, "exclude": info.exclude, "json_schema_extra": info.json_schema_extra, - } + }, ) f = next( @@ -120,7 +119,7 @@ def _patch_fastapi_dependent(dependant: "Dependant") -> "Dependant": "ge": info.field_info.ge, "lt": info.field_info.lt, "le": info.field_info.le, - } + }, ) f = Field(**field_data) # type: ignore[pydantic-field,unused-ignore] @@ -134,15 +133,18 @@ def _patch_fastapi_dependent(dependant: "Dependant") -> "Dependant": ) dependant.custom_fields = {} # type: ignore[attr-defined] - dependant.flat_params = params_unique # type: ignore[attr-defined] + dependant.flat_params = [ # type: ignore[attr-defined] + OptionItem(field_name=name, field_type=type_, default_value=default) + for name, (type_, default) in params_unique.items() + ] return dependant def has_forbidden_types( orig_call: Callable[..., Any], - forbidden_types: Tuple[Any, ...], -) -> Set[Any]: + forbidden_types: tuple[Any, ...], +) -> set[Any]: """Check if faststream.Depends is used in the handler.""" endpoint_signature = get_typed_signature(orig_call) signature_params = endpoint_signature.parameters diff --git a/faststream/broker/fastapi/route.py b/faststream/_internal/fastapi/route.py similarity index 85% rename from faststream/broker/fastapi/route.py rename to faststream/_internal/fastapi/route.py index b3b9fb715b..d145363f28 100644 --- a/faststream/broker/fastapi/route.py +++ b/faststream/_internal/fastapi/route.py @@ -1,28 +1,24 @@ import asyncio import inspect +from collections.abc import Awaitable, Callable, Iterable from contextlib import AsyncExitStack from functools import wraps from itertools import dropwhile from typing import ( TYPE_CHECKING, Any, - Awaitable, - Callable, - Iterable, - List, Optional, Union, ) -from fast_depends.dependencies import model +from fast_depends.dependencies import Dependant from fastapi.routing import run_endpoint_function, serialize_response from starlette.requests import Request -from faststream.broker.fastapi.get_dependant import has_forbidden_types -from faststream.broker.response import Response, ensure_response -from faststream.broker.types import P_HandlerParams, T_HandlerReturn +from faststream._internal.context import Context +from faststream._internal.types import P_HandlerParams, T_HandlerReturn from faststream.exceptions import SetupError -from faststream.utils.context.types import Context as FSContext +from faststream.response import Response, ensure_response from ._compat import ( FASTAPI_V106, @@ -33,6 +29,7 @@ from .config import FastAPIConfig from .get_dependant import ( get_fastapi_native_dependant, + has_forbidden_types, is_faststream_decorated, mark_faststream_decorated, ) @@ -40,11 +37,12 @@ if TYPE_CHECKING: from fastapi import params from fastapi._compat import ModelField - from fastapi.dependencies.models import Dependant + from fastapi.dependencies.models import Dependant as FastAPIDependant from fastapi.types import IncEx - from faststream.broker.message import StreamMessage as NativeMessage - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict + from faststream._internal.di import FastDependsConfig + from faststream.message import StreamMessage as NativeMessage class StreamMessage(Request): @@ -52,14 +50,14 @@ class StreamMessage(Request): scope: "AnyDict" _cookies: "AnyDict" - _headers: "AnyDict" # type: ignore - _body: Union["AnyDict", List[Any]] # type: ignore - _query_params: "AnyDict" # type: ignore + _headers: "AnyDict" # type: ignore[assignment] + _body: Union["AnyDict", list[Any]] # type: ignore[assignment] + _query_params: "AnyDict" # type: ignore[assignment] def __init__( self, *, - body: Union["AnyDict", List[Any]], + body: Union["AnyDict", list[Any]], headers: "AnyDict", path: "AnyDict", ) -> None: @@ -84,15 +82,16 @@ def wrap_callable_to_fastapi_compatible( response_model_exclude_unset: bool, response_model_exclude_defaults: bool, response_model_exclude_none: bool, + config: "FastDependsConfig", ) -> Callable[["NativeMessage[Any]"], Awaitable[Any]]: - if has_forbidden_types(user_callable, (model.Depends,)): + if has_forbidden_types(user_callable, (Dependant,)): msg = ( f"Incorrect `faststream.Depends` usage at `{user_callable.__name__}`. " "For FastAPI integration use `fastapi.Depends` instead." ) raise SetupError(msg) - if has_forbidden_types(user_callable, (FSContext,)): + if has_forbidden_types(user_callable, (Context,)): msg = ( f"Incorrect `faststream.Context` usage at `{user_callable.__name__}`. " "For FastAPI integration use `faststream.[broker].fastapi.Context` instead." @@ -121,6 +120,7 @@ def wrap_callable_to_fastapi_compatible( response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + config=config, ) mark_faststream_decorated(parsed_callable) @@ -129,7 +129,7 @@ def wrap_callable_to_fastapi_compatible( def build_faststream_to_fastapi_parser( *, - dependent: "Dependant", + dependent: "FastAPIDependant", fastapi_config: FastAPIConfig, response_field: Optional["ModelField"], response_model_include: Optional["IncEx"], @@ -138,6 +138,7 @@ def build_faststream_to_fastapi_parser( response_model_exclude_unset: bool, response_model_exclude_defaults: bool, response_model_exclude_none: bool, + config: "FastDependsConfig", ) -> Callable[["NativeMessage[Any]"], Awaitable[Any]]: """Creates a session for handling requests.""" assert dependent.call # nosec B101 @@ -168,7 +169,7 @@ async def parsed_consumer(message: "NativeMessage[Any]") -> Any: """Wrapper, that parser FastStream message to FastAPI compatible one.""" body = await message.decode() - fastapi_body: Union[AnyDict, List[Any]] + fastapi_body: AnyDict | list[Any] if first_arg is not None: if isinstance(body, dict): path = fastapi_body = body or {} @@ -179,14 +180,14 @@ async def parsed_consumer(message: "NativeMessage[Any]") -> Any: stream_message = StreamMessage( body=fastapi_body, - headers=message.headers, + headers={"context__": config.context, **message.headers}, path={**path, **message.path}, ) else: stream_message = StreamMessage( body={}, - headers={}, + headers={"context__": config.context}, path={}, ) @@ -197,7 +198,7 @@ async def parsed_consumer(message: "NativeMessage[Any]") -> Any: def make_fastapi_execution( *, - dependent: "Dependant", + dependent: "FastAPIDependant", fastapi_config: FastAPIConfig, response_field: Optional["ModelField"], response_model_include: Optional["IncEx"], @@ -261,6 +262,7 @@ async def app( return response - raise AssertionError("unreachable") + msg = "unreachable" + raise AssertionError(msg) return app diff --git a/faststream/_internal/fastapi/router.py b/faststream/_internal/fastapi/router.py new file mode 100644 index 0000000000..56f33cc621 --- /dev/null +++ b/faststream/_internal/fastapi/router.py @@ -0,0 +1,522 @@ +import json +import warnings +from abc import abstractmethod +from collections.abc import ( + AsyncIterator, + Awaitable, + Callable, + Iterable, + Mapping, + Sequence, +) +from contextlib import asynccontextmanager +from enum import Enum +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Optional, + Union, + cast, + overload, +) + +from fastapi.datastructures import Default +from fastapi.responses import HTMLResponse +from fastapi.routing import APIRoute, APIRouter +from fastapi.utils import generate_unique_id +from starlette.responses import JSONResponse, Response +from starlette.routing import BaseRoute, _DefaultLifespan + +from faststream._internal.application import StartAbleApplication +from faststream._internal.broker import BrokerRouter +from faststream._internal.di.config import FastDependsConfig +from faststream._internal.types import ( + MsgType, + P_HandlerParams, + T_HandlerReturn, +) +from faststream._internal.utils.functions import fake_context, to_async +from faststream.middlewares import BaseMiddleware +from faststream.specification.asyncapi.site import get_asyncapi_html + +from .config import FastAPIConfig +from .get_dependant import get_fastapi_dependant +from .route import wrap_callable_to_fastapi_compatible + +if TYPE_CHECKING: + from types import TracebackType + + from fastapi import FastAPI, params + from fastapi.background import BackgroundTasks + from fastapi.types import IncEx + from starlette import routing + from starlette.types import ASGIApp, AppType, Lifespan + + from faststream._internal.basic_types import AnyDict + from faststream._internal.broker import BrokerUsecase + from faststream._internal.endpoint.call_wrapper import HandlerCallWrapper + from faststream._internal.endpoint.publisher import PublisherProto + from faststream._internal.proto import NameRequired + from faststream._internal.types import BrokerMiddleware + from faststream.message import StreamMessage + from faststream.specification.base.specification import Specification + from faststream.specification.schema.extra import Tag, TagDict + + +class _BackgroundMiddleware(BaseMiddleware): + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> bool | None: + if not exc_type and ( + background := cast( + "BackgroundTasks | None", + getattr(self.context.get_local("message"), "background", None), + ) + ): + await background() + + return await super().after_processed(exc_type, exc_val, exc_tb) + + +class StreamRouter( + APIRouter, + StartAbleApplication, + Generic[MsgType], +): + """A class to route streams.""" + + broker_class: type["BrokerUsecase[MsgType, Any]"] + broker: "BrokerUsecase[MsgType, Any]" + docs_router: APIRouter | None + _after_startup_hooks: list[Callable[[Any], Awaitable[Mapping[str, Any] | None]]] + _on_shutdown_hooks: list[Callable[[Any], Awaitable[None]]] + schema: Optional["Specification"] + + title: str + description: str + version: str + license: Optional["AnyDict"] + contact: Optional["AnyDict"] + + def __init__( + self, + *connection_args: Any, + middlewares: Sequence["BrokerMiddleware[MsgType]"] = (), + prefix: str = "", + tags: list[str | Enum] | None = None, + dependencies: Sequence["params.Depends"] | None = None, + default_response_class: type["Response"] = Default(JSONResponse), + responses: dict[int | str, "AnyDict"] | None = None, + callbacks: list["routing.BaseRoute"] | None = None, + routes: list["routing.BaseRoute"] | None = None, + redirect_slashes: bool = True, + default: Optional["ASGIApp"] = None, + dependency_overrides_provider: Any | None = None, + route_class: type["APIRoute"] = APIRoute, + on_startup: Sequence[Callable[[], Any]] | None = None, + on_shutdown: Sequence[Callable[[], Any]] | None = None, + deprecated: bool | None = None, + include_in_schema: bool = True, + setup_state: bool = True, + lifespan: Optional["Lifespan[Any]"] = None, + generate_unique_id_function: Callable[["APIRoute"], str] = Default( + generate_unique_id, + ), + # Specification information + specification_tags: Iterable[Union["Tag", "TagDict"]] = (), + schema_url: str | None = "/asyncapi", + **connection_kwars: Any, + ) -> None: + assert ( # nosec B101 + self.broker_class + ), "You should specify `broker_class` at your implementation" + + broker = self.broker_class( + *connection_args, + middlewares=( + *middlewares, + # allow to catch background exceptions in user middlewares + _BackgroundMiddleware, + ), + tags=specification_tags, + apply_types=False, + **connection_kwars, + ) + + self._init_setupable_( + broker, + config=FastDependsConfig(get_dependent=get_fastapi_dependant), + ) + + self.setup_state = setup_state + + # Specification information + # Empty + self.terms_of_service = None + self.identifier = None + self.specification_tags = None + self.external_docs = None + # parse from FastAPI app on startup + self.title = "" + self.version = "" + self.description = "" + self.license = None + self.contact = None + + self.schema = None + + super().__init__( + prefix=prefix, + tags=tags, + dependencies=dependencies, + default_response_class=default_response_class, + responses=responses, + callbacks=callbacks, + routes=routes, + redirect_slashes=redirect_slashes, + default=default, + dependency_overrides_provider=dependency_overrides_provider, + route_class=route_class, + deprecated=deprecated, + include_in_schema=include_in_schema, + generate_unique_id_function=generate_unique_id_function, + lifespan=self._wrap_lifespan(lifespan), + on_startup=on_startup, + on_shutdown=on_shutdown, + ) + + self.fastapi_config = FastAPIConfig( + dependency_overrides_provider=dependency_overrides_provider + ) + + if self.include_in_schema: + self.docs_router = self._asyncapi_router(schema_url) + else: + self.docs_router = None + + self._after_startup_hooks = [] + self._on_shutdown_hooks = [] + + self._lifespan_started = False + + def _add_api_mq_route( + self, + dependencies: Iterable["params.Depends"], + response_model: Any, + response_model_include: Optional["IncEx"], + response_model_exclude: Optional["IncEx"], + response_model_by_alias: bool, + response_model_exclude_unset: bool, + response_model_exclude_defaults: bool, + response_model_exclude_none: bool, + ) -> Callable[ + [Callable[..., Any]], + Callable[["StreamMessage[Any]"], Awaitable[Any]], + ]: + """Decorator before `broker.subscriber`, that wraps function to FastAPI-compatible one.""" + + def wrapper( + endpoint: Callable[..., Any], + ) -> Callable[["StreamMessage[Any]"], Awaitable[Any]]: + """Patch user function to make it FastAPI-compatible.""" + return wrap_callable_to_fastapi_compatible( + user_callable=endpoint, + dependencies=dependencies, + response_model=response_model, + response_model_include=response_model_include, + response_model_exclude=response_model_exclude, + response_model_by_alias=response_model_by_alias, + response_model_exclude_unset=response_model_exclude_unset, + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, + config=self.config, + fastapi_config=self.fastapi_config, + ) + + return wrapper + + def subscriber( + self, + *extra: Union["NameRequired", str], + dependencies: Iterable["params.Depends"], + response_model: Any, + response_model_include: Optional["IncEx"], + response_model_exclude: Optional["IncEx"], + response_model_by_alias: bool, + response_model_exclude_unset: bool, + response_model_exclude_defaults: bool, + response_model_exclude_none: bool, + **broker_kwargs: Any, + ) -> Callable[ + [Callable[P_HandlerParams, T_HandlerReturn]], + "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", + ]: + """A function decorator for subscribing to a message queue.""" + dependencies = (*self.dependencies, *dependencies) + + sub = self.broker.subscriber( # type: ignore[call-arg] + *extra, # type: ignore[arg-type] + dependencies=dependencies, + **broker_kwargs, + ) + + sub._call_decorators = ( # type: ignore[attr-defined] + self._add_api_mq_route( + dependencies=dependencies, + response_model=response_model, + response_model_include=response_model_include, + response_model_exclude=response_model_exclude, + response_model_by_alias=response_model_by_alias, + response_model_exclude_unset=response_model_exclude_unset, + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, + ), + ) + + return sub + + def _wrap_lifespan( + self, + lifespan: Optional["Lifespan[Any]"] = None, + ) -> "Lifespan[Any]": + lifespan_context = lifespan if lifespan is not None else _DefaultLifespan(self) + + @asynccontextmanager + async def start_broker_lifespan( + app: "FastAPI", + ) -> AsyncIterator[Mapping[str, Any] | None]: + """Starts the lifespan of a broker.""" + self.fastapi_config.set_application(app) + + if self.docs_router: + self.title = app.title + self.description = app.description + self.version = app.version + self.contact = app.contact + self.license = app.license_info + + from faststream.specification.asyncapi import AsyncAPI + + self.schema = AsyncAPI( + self.broker, + title=self.title, + description=self.description, + app_version=self.version, + contact=self.contact, + license=self.license, + ) + + app.include_router(self.docs_router) + + async with lifespan_context(app) as maybe_context: + lifespan_extra = {"broker": self.broker, **(maybe_context or {})} + + if not self._lifespan_started: + await self._start_broker() + self._lifespan_started = True + else: + warnings.warn( + "Specifying 'lifespan_context' manually is no longer necessary with FastAPI >= 0.112.2.", + category=RuntimeWarning, + stacklevel=2, + ) + + for h in self._after_startup_hooks: + lifespan_extra.update(await h(app) or {}) + + try: + if self.setup_state: + yield lifespan_extra + else: + # NOTE: old asgi compatibility + yield None + + for h in self._on_shutdown_hooks: + await h(app) + + finally: + await self.broker.close() + + return start_broker_lifespan # type: ignore[return-value] + + @overload + def after_startup( + self, + func: Callable[["AppType"], Mapping[str, Any]], + ) -> Callable[["AppType"], Mapping[str, Any]]: ... + + @overload + def after_startup( + self, + func: Callable[["AppType"], Awaitable[Mapping[str, Any]]], + ) -> Callable[["AppType"], Awaitable[Mapping[str, Any]]]: ... + + @overload + def after_startup( + self, + func: Callable[["AppType"], None], + ) -> Callable[["AppType"], None]: ... + + @overload + def after_startup( + self, + func: Callable[["AppType"], Awaitable[None]], + ) -> Callable[["AppType"], Awaitable[None]]: ... + + def after_startup( + self, + func: Callable[["AppType"], Mapping[str, Any]] | Callable[["AppType"], Awaitable[Mapping[str, Any]]] | Callable[["AppType"], None] | Callable[["AppType"], Awaitable[None]], + ) -> Callable[["AppType"], Mapping[str, Any]] | Callable[["AppType"], Awaitable[Mapping[str, Any]]] | Callable[["AppType"], None] | Callable[["AppType"], Awaitable[None]]: + """Register a function to be executed after startup.""" + self._after_startup_hooks.append(to_async(func)) + return func + + @overload + def on_broker_shutdown( + self, + func: Callable[["AppType"], None], + ) -> Callable[["AppType"], None]: ... + + @overload + def on_broker_shutdown( + self, + func: Callable[["AppType"], Awaitable[None]], + ) -> Callable[["AppType"], Awaitable[None]]: ... + + def on_broker_shutdown( + self, + func: Callable[["AppType"], None] | Callable[["AppType"], Awaitable[None]], + ) -> Callable[["AppType"], None] | Callable[["AppType"], Awaitable[None]]: + """Register a function to be executed before broker stop.""" + self._on_shutdown_hooks.append(to_async(func)) + return func + + @abstractmethod + def publisher(self) -> "PublisherProto[MsgType]": + """Create Publisher object.""" + raise NotImplementedError + + def _asyncapi_router(self, schema_url: str | None) -> APIRouter | None: + """Creates an API router for serving AsyncAPI documentation.""" + if not self.include_in_schema or not schema_url: + return None + + def download_app_json_schema() -> Response: + assert ( # nosec B101 + self.schema + ), "You need to run application lifespan at first" + + return Response( + content=json.dumps(self.schema.to_jsonable(), indent=2), + headers={"Content-Type": "application/octet-stream"}, + ) + + def download_app_yaml_schema() -> Response: + assert ( # nosec B101 + self.schema + ), "You need to run application lifespan at first" + + return Response( + content=self.schema.to_yaml(), + headers={ + "Content-Type": "application/octet-stream", + }, + ) + + def serve_asyncapi_schema( + sidebar: bool = True, + info: bool = True, + servers: bool = True, + operations: bool = True, + messages: bool = True, + schemas: bool = True, + errors: bool = True, + expandMessageExamples: bool = True, + ) -> HTMLResponse: + """Serve the AsyncAPI schema as an HTML response.""" + assert ( # nosec B101 + self.schema + ), "You need to run application lifespan at first" + + return HTMLResponse( + content=get_asyncapi_html( + self.schema.schema, + sidebar=sidebar, + info=info, + servers=servers, + operations=operations, + messages=messages, + schemas=schemas, + errors=errors, + expand_message_examples=expandMessageExamples, + ), + ) + + docs_router = APIRouter( + prefix=self.prefix, + tags=["asyncapi"], + redirect_slashes=self.redirect_slashes, + default=self.default, + deprecated=self.deprecated, + ) + docs_router.get(schema_url)(serve_asyncapi_schema) + docs_router.get(f"{schema_url}.json")(download_app_json_schema) + docs_router.get(f"{schema_url}.yaml")(download_app_yaml_schema) + return docs_router + + def include_router( # type: ignore[override] + self, + router: Union["StreamRouter[MsgType]", "BrokerRouter[MsgType]"], + *, + prefix: str = "", + tags: list[str | Enum] | None = None, + dependencies: Sequence["params.Depends"] | None = None, + default_response_class: type[Response] = Default(JSONResponse), + responses: dict[int | str, "AnyDict"] | None = None, + callbacks: list["BaseRoute"] | None = None, + deprecated: bool | None = None, + include_in_schema: bool = True, + generate_unique_id_function: Callable[["APIRoute"], str] = Default( + generate_unique_id, + ), + ) -> None: + """Includes a router in the API.""" + if isinstance(router, BrokerRouter): + for sub in router.subscribers: + sub._call_decorators = ( # type: ignore[attr-defined] + self._add_api_mq_route( + dependencies=(), + response_model=Default(None), + response_model_include=None, + response_model_exclude=None, + response_model_by_alias=True, + response_model_exclude_unset=False, + response_model_exclude_defaults=False, + response_model_exclude_none=False, + ), + ) + + self.broker.include_router(router) + return + + if isinstance(router, StreamRouter): # pragma: no branch + router.lifespan_context = fake_context + self.broker.include_router(router.broker) + router.fastapi_config = self.fastapi_config + + super().include_router( + router=router, + prefix=prefix, + tags=tags, + dependencies=dependencies, + default_response_class=default_response_class, + responses=responses, + callbacks=callbacks, + deprecated=deprecated, + include_in_schema=include_in_schema, + generate_unique_id_function=generate_unique_id_function, + ) diff --git a/faststream/_internal/logger/__init__.py b/faststream/_internal/logger/__init__.py new file mode 100644 index 0000000000..795b67f46f --- /dev/null +++ b/faststream/_internal/logger/__init__.py @@ -0,0 +1,11 @@ +from .logging import logger +from .params_storage import DefaultLoggerStorage, LoggerParamsStorage +from .state import LoggerState, make_logger_state + +__all__ = ( + "DefaultLoggerStorage", + "LoggerParamsStorage", + "LoggerState", + "logger", + "make_logger_state", +) diff --git a/faststream/log/formatter.py b/faststream/_internal/logger/formatter.py similarity index 91% rename from faststream/log/formatter.py rename to faststream/_internal/logger/formatter.py index fe1aa3a267..f79966f0e1 100644 --- a/faststream/log/formatter.py +++ b/faststream/_internal/logger/formatter.py @@ -1,6 +1,6 @@ import logging import sys -from typing import Literal, Optional +from typing import Literal COLORED_LEVELS = { logging.DEBUG: "\033[36mDEBUG\033[0m", @@ -21,10 +21,10 @@ class ColourizedFormatter(logging.Formatter): def __init__( self, - fmt: Optional[str] = None, - datefmt: Optional[str] = None, + fmt: str | None = None, + datefmt: str | None = None, style: Literal["%", "{", "$"] = "%", - use_colors: Optional[bool] = None, + use_colors: bool | None = None, ) -> None: """Initialize the formatter with specified format strings. @@ -37,7 +37,7 @@ def __init__( use one of %-formatting, :meth:`str.format` (``{}``) formatting or :class:`string.Template` formatting in your format string. """ - if use_colors in (True, False): + if use_colors in {True, False}: self.use_colors = use_colors else: self.use_colors = sys.stdout.isatty() diff --git a/faststream/_internal/logger/logger_proxy.py b/faststream/_internal/logger/logger_proxy.py new file mode 100644 index 0000000000..b21f04ec59 --- /dev/null +++ b/faststream/_internal/logger/logger_proxy.py @@ -0,0 +1,103 @@ +from abc import abstractmethod +from collections.abc import Mapping +from typing import Any, Optional + +from faststream._internal.basic_types import LoggerProto +from faststream.exceptions import IncorrectState + + +class LoggerObject(LoggerProto): + logger: Optional["LoggerProto"] + + @abstractmethod + def __bool__(self) -> bool: ... + + +class NotSetLoggerObject(LoggerObject): + """Default logger proxy for state. + + Raises an error if user tries to log smth before state setup. + """ + + def __init__(self) -> None: + self.logger = None + + def __bool__(self) -> bool: + return False + + def __repr__(self) -> str: + return f"{self.__class__.__name__}()" + + def log( + self, + level: int, + msg: Any, + /, + *, + exc_info: Any = None, + extra: Mapping[str, Any] | None = None, + ) -> None: + err_msg = "Logger object not set. Please, call `_setup_logger_state` of parent broker state." + raise IncorrectState(err_msg) + + +class EmptyLoggerObject(LoggerObject): + """Empty logger proxy for state. + + Will be used if user setup `logger=None`. + """ + + def __init__(self) -> None: + self.logger = None + + def __bool__(self) -> bool: + return True + + def __repr__(self) -> str: + return f"{self.__class__.__name__}()" + + def log( + self, + level: int, + msg: Any, + /, + *, + exc_info: Any = None, + extra: Mapping[str, Any] | None = None, + ) -> None: + pass + + +class RealLoggerObject(LoggerObject): + """Empty logger proxy for state. + + Will be used if user setup custom `logger` (.params_storage.ManualLoggerStorage) + or in default logger case (.params_storage.DefaultLoggerStorage). + """ + + logger: "LoggerProto" + + def __init__(self, logger: "LoggerProto") -> None: + self.logger = logger + + def __bool__(self) -> bool: + return True + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(logger={self.logger})" + + def log( + self, + level: int, + msg: Any, + /, + *, + exc_info: Any = None, + extra: Mapping[str, Any] | None = None, + ) -> None: + self.logger.log( + level, + msg, + extra=extra, + exc_info=exc_info, + ) diff --git a/faststream/_internal/logger/logging.py b/faststream/_internal/logger/logging.py new file mode 100644 index 0000000000..8c1ffa87c2 --- /dev/null +++ b/faststream/_internal/logger/logging.py @@ -0,0 +1,99 @@ +import logging +import sys +from collections.abc import Mapping +from logging import LogRecord +from typing import TYPE_CHECKING, TextIO + +from .formatter import ColourizedFormatter + +if TYPE_CHECKING: + from faststream._internal.context.repository import ContextRepo + + +class ExtendedFilter(logging.Filter): + def __init__( + self, + default_context: Mapping[str, str], + message_id_ln: int, + context: "ContextRepo", + name: str = "", + ) -> None: + self.default_context = default_context + self.message_id_ln = message_id_ln + self.context = context + super().__init__(name) + + def filter(self, record: LogRecord) -> bool: + if is_suitable := super().filter(record): + log_context: Mapping[str, str] = self.context.get_local( + "log_context", + self.default_context, + ) + + for k, v in log_context.items(): + value = getattr(record, k, v) + setattr(record, k, value) + + record.message_id = getattr(record, "message_id", "")[: self.message_id_ln] + + return is_suitable + + +def get_broker_logger( + name: str, + default_context: Mapping[str, str], + message_id_ln: int, + fmt: str, + context: "ContextRepo", + log_level: int, +) -> logging.Logger: + logger = get_logger( + name=f"faststream.access.{name}", + log_level=log_level, + stream=sys.stdout, + fmt=fmt, + ) + logger.addFilter(ExtendedFilter(default_context, message_id_ln, context=context)) + return logger + + +def get_logger(name: str, log_level: int, stream: "TextIO", fmt: str) -> logging.Logger: + logger = logging.getLogger(name) + logger.setLevel(log_level) + logger.propagate = False + set_logger_fmt(logger, stream=stream, fmt=fmt) + return logger + + +def set_logger_fmt( + logger: logging.Logger, + stream: "TextIO", + fmt: str, +) -> None: + if _handler_exists(logger): + return + + handler = logging.StreamHandler(stream=stream) + handler.setFormatter( + ColourizedFormatter( + fmt=fmt, + use_colors=True, + ), + ) + logger.addHandler(handler) + + +def _handler_exists(logger: logging.Logger) -> bool: + # Check if a StreamHandler for sys.stdout already exists in the logger. + for handler in logger.handlers: + if isinstance(handler, logging.StreamHandler) and handler.stream == sys.stdout: + return True + return False + + +logger = get_logger( + name="faststream", + log_level=logging.INFO, + stream=sys.stderr, + fmt="%(asctime)s %(levelname)8s - %(message)s", +) diff --git a/faststream/_internal/logger/params_storage.py b/faststream/_internal/logger/params_storage.py new file mode 100644 index 0000000000..ff4c966442 --- /dev/null +++ b/faststream/_internal/logger/params_storage.py @@ -0,0 +1,78 @@ +import logging +from abc import abstractmethod +from typing import TYPE_CHECKING, Optional, Protocol +from weakref import WeakSet + +from faststream._internal.constants import EMPTY + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.context import ContextRepo + + +def make_logger_storage( + logger: Optional["LoggerProto"], + default_storage_cls: type["DefaultLoggerStorage"], +) -> "LoggerParamsStorage": + if logger is EMPTY: + return default_storage_cls() + + return EmptyLoggerStorage() if logger is None else ManualLoggerStorage(logger) + + +class LoggerParamsStorage(Protocol): + def register_subscriber(self, params: "AnyDict") -> None: ... + + def get_logger(self, *, context: "ContextRepo") -> Optional["LoggerProto"]: ... + + def set_level(self, level: int) -> None: ... + + +class EmptyLoggerStorage(LoggerParamsStorage): + def register_subscriber(self, params: "AnyDict") -> None: + pass + + def get_logger(self, *, context: "ContextRepo") -> None: + return None + + def set_level(self, level: int) -> None: + pass + + +class ManualLoggerStorage(LoggerParamsStorage): + def __init__(self, logger: "LoggerProto") -> None: + self.__logger = logger + + def register_subscriber(self, params: "AnyDict") -> None: + pass + + def get_logger(self, *, context: "ContextRepo") -> "LoggerProto": + return self.__logger + + def set_level(self, level: int) -> None: + if getattr(self.__logger, "setLevel", None): + self.__logger.setLevel(level) # type: ignore[attr-defined] + + +class DefaultLoggerStorage(LoggerParamsStorage): + def __init__(self) -> None: + # will be used to build logger in `get_logger` method + self.logger_log_level = logging.INFO + + self._logger_ref = WeakSet[logging.Logger]() + + @abstractmethod + def get_logger(self, *, context: "ContextRepo") -> "LoggerProto": + raise NotImplementedError + + def _get_logger_ref(self) -> logging.Logger | None: + if self._logger_ref: + return next(iter(self._logger_ref)) + + return None + + def set_level(self, level: int) -> None: + if lg := self._get_logger_ref(): + lg.setLevel(level) + + self.logger_log_level = level diff --git a/faststream/_internal/logger/state.py b/faststream/_internal/logger/state.py new file mode 100644 index 0000000000..c117af1fca --- /dev/null +++ b/faststream/_internal/logger/state.py @@ -0,0 +1,75 @@ +import logging +from typing import TYPE_CHECKING, Optional + +from .logger_proxy import ( + EmptyLoggerObject, + LoggerObject, + NotSetLoggerObject, + RealLoggerObject, +) +from .params_storage import ( + DefaultLoggerStorage, + EmptyLoggerStorage, + LoggerParamsStorage, + make_logger_storage, +) + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.context import ContextRepo + + +def make_logger_state( + logger: Optional["LoggerProto"], + log_level: int, + default_storage_cls: type["DefaultLoggerStorage"], +) -> "LoggerState": + storage = make_logger_storage( + logger=logger, + default_storage_cls=default_storage_cls, + ) + + return LoggerState( + log_level=log_level, + storage=storage, + ) + + +class LoggerState: + def __init__( + self, + log_level: int = logging.INFO, + storage: Optional["LoggerParamsStorage"] = None, + ) -> None: + self.log_level = log_level + self.params_storage = storage or EmptyLoggerStorage() + + self.logger: LoggerObject = NotSetLoggerObject() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(log_level={self.log_level}, logger={self.logger})" + + def set_level(self, level: int) -> None: + self.log_level = level + self.params_storage.set_level(level) + + def log( + self, + message: str, + log_level: int | None = None, + extra: Optional["AnyDict"] = None, + exc_info: BaseException | None = None, + ) -> None: + self.logger.log( + (log_level or self.log_level), + message, + extra=extra, + exc_info=exc_info, + ) + + def _setup(self, context: "ContextRepo", /) -> None: + if not self.logger: + if logger := self.params_storage.get_logger(context=context): + self.logger = RealLoggerObject(logger) + else: + self.logger = EmptyLoggerObject() diff --git a/faststream/_internal/middlewares.py b/faststream/_internal/middlewares.py new file mode 100644 index 0000000000..d0a2ceb00b --- /dev/null +++ b/faststream/_internal/middlewares.py @@ -0,0 +1,117 @@ +from collections.abc import Awaitable, Callable +from typing import TYPE_CHECKING, Any, Generic, Optional + +from typing_extensions import Self + +from faststream._internal.types import AnyMsg, PublishCommandType + +if TYPE_CHECKING: + from types import TracebackType + + from faststream._internal.basic_types import AsyncFuncAny + from faststream._internal.context.repository import ContextRepo + from faststream.message import StreamMessage + + +class BaseMiddleware(Generic[PublishCommandType, AnyMsg]): + """A base middleware class.""" + + def __init__( + self, + msg: AnyMsg | None, + /, + *, + context: "ContextRepo", + ) -> None: + self.msg = msg + self.context = context + + async def on_receive(self) -> None: + """Hook to call on message receive.""" + + async def after_processed( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> bool | None: + """Asynchronously called after processing.""" + return False + + async def __aenter__(self) -> Self: + await self.on_receive() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> bool | None: + """Exit the asynchronous context manager.""" + return await self.after_processed(exc_type, exc_val, exc_tb) + + async def on_consume( + self, + msg: "StreamMessage[AnyMsg]", + ) -> "StreamMessage[AnyMsg]": + """This option was deprecated and will be removed in 0.7.0. Please, use `consume_scope` instead.""" + return msg + + async def after_consume(self, err: Exception | None) -> None: + """This option was deprecated and will be removed in 0.7.0. Please, use `consume_scope` instead.""" + if err is not None: + raise err + + async def consume_scope( + self, + call_next: "AsyncFuncAny", + msg: "StreamMessage[AnyMsg]", + ) -> Any: + """Asynchronously consumes a message and returns an asynchronous iterator of decoded messages.""" + err: Exception | None = None + try: + result = await call_next(await self.on_consume(msg)) + + except Exception as e: + err = e + + else: + return result + + finally: + await self.after_consume(err) + + async def on_publish( + self, + msg: PublishCommandType, + ) -> PublishCommandType: + """This option was deprecated and will be removed in 0.7.0. Please, use `publish_scope` instead.""" + return msg + + async def after_publish( + self, + err: Exception | None, + ) -> None: + """This option was deprecated and will be removed in 0.7.0. Please, use `publish_scope` instead.""" + if err is not None: + raise err + + async def publish_scope( + self, + call_next: Callable[[PublishCommandType], Awaitable[Any]], + cmd: PublishCommandType, + ) -> Any: + """Publish a message and return an async iterator.""" + err: Exception | None = None + try: + result = await call_next(await self.on_publish(cmd)) + + except Exception as e: + err = e + + else: + return result + + finally: + await self.after_publish(err) diff --git a/faststream/_internal/producer.py b/faststream/_internal/producer.py new file mode 100644 index 0000000000..6f06012be8 --- /dev/null +++ b/faststream/_internal/producer.py @@ -0,0 +1,66 @@ +from abc import abstractmethod +from typing import TYPE_CHECKING, Any, Protocol + +from faststream.exceptions import IncorrectState + +if TYPE_CHECKING: + from faststream._internal.types import AsyncCallable + from faststream.response import PublishCommand + + +class ProducerProto(Protocol): + _parser: "AsyncCallable" + _decoder: "AsyncCallable" + + @abstractmethod + async def publish(self, cmd: "PublishCommand") -> Any | None: + """Publishes a message asynchronously.""" + ... + + @abstractmethod + async def request(self, cmd: "PublishCommand") -> Any: + """Publishes a message synchronously.""" + ... + + @abstractmethod + async def publish_batch(self, cmd: "PublishCommand") -> Any: + """Publishes a messages batch asynchronously.""" + ... + + +class ProducerFactory(Protocol): + def __call__( + self, parser: "AsyncCallable", decoder: "AsyncCallable" + ) -> ProducerProto: ... + + +class ProducerUnset(ProducerProto): + msg = "Producer is unset yet. You should set producer in broker initial method." + + def __bool__(self) -> bool: + return False + + @property + def _parser(self) -> "AsyncCallable": + raise IncorrectState(self.msg) + + @_parser.setter + def _parser(self, value: "AsyncCallable", /) -> "AsyncCallable": + raise IncorrectState(self.msg) + + @property + def _decoder(self) -> "AsyncCallable": + raise IncorrectState(self.msg) + + @_decoder.setter + def _decoder(self, value: "AsyncCallable", /) -> "AsyncCallable": + raise IncorrectState(self.msg) + + async def publish(self, cmd: "PublishCommand") -> Any | None: + raise IncorrectState(self.msg) + + async def request(self, cmd: "PublishCommand") -> Any: + raise IncorrectState(self.msg) + + async def publish_batch(self, cmd: "PublishCommand") -> None: + raise IncorrectState(self.msg) diff --git a/faststream/_internal/proto.py b/faststream/_internal/proto.py new file mode 100644 index 0000000000..871db629d4 --- /dev/null +++ b/faststream/_internal/proto.py @@ -0,0 +1,49 @@ +from typing import Any, TypeVar, overload + +from typing_extensions import Self + +NameRequiredCls = TypeVar("NameRequiredCls", bound="NameRequired") + + +class NameRequired: + """Required name option object.""" + + def __eq__(self, value: object, /) -> bool: + """Compares the current object with another object for equality.""" + if value is None: + return False + + if not isinstance(value, NameRequired): + return NotImplemented + + return self.name == value.name + + def __init__(self, name: str) -> None: + self.name = name + + @overload + @classmethod + def validate( + cls: type[NameRequiredCls], + value: str | NameRequiredCls, + **kwargs: Any, + ) -> NameRequiredCls: ... + + @overload + @classmethod + def validate( + cls: type[NameRequiredCls], + value: None, + **kwargs: Any, + ) -> None: ... + + @classmethod + def validate( + cls, + value: str | Self | None, + **kwargs: Any, + ) -> Self | None: + """Factory to create object.""" + if value is not None and isinstance(value, str): + value = cls(value, **kwargs) + return value diff --git a/faststream/_internal/testing/__init__.py b/faststream/_internal/testing/__init__.py new file mode 100644 index 0000000000..b4f7ac676d --- /dev/null +++ b/faststream/_internal/testing/__init__.py @@ -0,0 +1,3 @@ +from faststream._internal.testing.app import TestApp + +__all__ = ("TestApp",) diff --git a/faststream/_internal/testing/app.py b/faststream/_internal/testing/app.py new file mode 100644 index 0000000000..f95bb0c19f --- /dev/null +++ b/faststream/_internal/testing/app.py @@ -0,0 +1,68 @@ +from contextlib import ExitStack +from functools import partial +from typing import TYPE_CHECKING, Optional + +from anyio.from_thread import start_blocking_portal + +if TYPE_CHECKING: + from types import TracebackType + + from faststream._internal.application import Application + from faststream._internal.basic_types import SettingField + + +class TestApp: + """A class to represent a test application.""" + + __test__ = False + + app: "Application" + _extra_options: dict[str, "SettingField"] + + def __init__( + self, + app: "Application", + run_extra_options: dict[str, "SettingField"] | None = None, + ) -> None: + self.app = app + self._extra_options = run_extra_options or {} + + def __enter__(self) -> "Application": + with ExitStack() as stack: + portal = stack.enter_context(start_blocking_portal()) + + lifespan_context = self.app.lifespan_context(**self._extra_options) + stack.enter_context(portal.wrap_async_context_manager(lifespan_context)) + portal.call(partial(self.app.start, **self._extra_options)) + + @stack.callback + def wait_shutdown() -> None: + portal.call(self.app.stop) + + self.exit_stack = stack.pop_all() + + return self.app + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> None: + self.exit_stack.close() + + async def __aenter__(self) -> "Application": + self.lifespan_scope = self.app.lifespan_context(**self._extra_options) + await self.lifespan_scope.__aenter__() + await self.app.start(**self._extra_options) + return self.app + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> None: + """Exit the asynchronous context manager.""" + await self.app.stop() + await self.lifespan_scope.__aexit__(exc_type, exc_val, exc_tb) diff --git a/faststream/_internal/testing/ast.py b/faststream/_internal/testing/ast.py new file mode 100644 index 0000000000..93186aff82 --- /dev/null +++ b/faststream/_internal/testing/ast.py @@ -0,0 +1,54 @@ +import ast +import traceback +from collections.abc import Iterator +from functools import lru_cache +from pathlib import Path +from typing import cast + + +def is_contains_context_name(scip_name: str, name: str) -> bool: + stack = traceback.extract_stack()[-3] + tree = _read_source_ast(stack.filename) + node = cast("ast.With | ast.AsyncWith", _find_ast_node(tree, stack.lineno)) + context_calls = _get_withitem_calls(node) + + try: + pos = context_calls.index(scip_name) + except ValueError: + pos = 1 + + return name in context_calls[pos:] + + +@lru_cache +def _read_source_ast(filename: str) -> ast.Module: + return ast.parse(Path(filename).read_text(encoding="utf-8")) + + +def _find_ast_node(module: ast.Module, lineno: int | None) -> ast.AST | None: + if lineno is not None: # pragma: no branch + for i in getattr(module, "body", ()): + if i.lineno == lineno: + return cast("ast.AST", i) + + r = _find_ast_node(i, lineno) + if r is not None: + return r + + return None + + +def _find_withitems(node: ast.With | ast.AsyncWith) -> Iterator[ast.withitem]: + if isinstance(node, (ast.With, ast.AsyncWith)): + yield from node.items + + for i in getattr(node, "body", ()): + yield from _find_withitems(i) + + +def _get_withitem_calls(node: ast.With | ast.AsyncWith) -> list[str]: + return [ + id + for i in _find_withitems(node) + if (id := getattr(i.context_expr.func, "id", None)) # type: ignore[attr-defined] + ] diff --git a/faststream/_internal/testing/broker.py b/faststream/_internal/testing/broker.py new file mode 100644 index 0000000000..a2892993b6 --- /dev/null +++ b/faststream/_internal/testing/broker.py @@ -0,0 +1,236 @@ +import warnings +from abc import abstractmethod +from collections.abc import AsyncGenerator, Generator, Iterator +from contextlib import asynccontextmanager, contextmanager +from functools import partial +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Optional, + TypeVar, +) +from unittest import mock +from unittest.mock import MagicMock + +from faststream._internal.broker import BrokerUsecase +from faststream._internal.logger.logger_proxy import RealLoggerObject +from faststream._internal.testing.app import TestApp +from faststream._internal.testing.ast import is_contains_context_name +from faststream._internal.utils.functions import FakeContext + +if TYPE_CHECKING: + from types import TracebackType + + from faststream._internal.configs import BrokerConfig + from faststream._internal.endpoint.subscriber import SubscriberProto + from faststream._internal.producer import ProducerProto + + +Broker = TypeVar("Broker", bound=BrokerUsecase[Any, Any]) + + +@contextmanager +def change_producer( + config: "BrokerConfig", producer: "ProducerProto" +) -> Generator[None, None, None]: + old_producer, config.producer = config.producer, producer + yield + config.producer = old_producer + + +class TestBroker(Generic[Broker]): + """A class to represent a test broker.""" + + # This is set so pytest ignores this class + __test__ = False + + def __init__( + self, + broker: Broker, + with_real: bool = False, + connect_only: bool | None = None, + ) -> None: + self.with_real = with_real + self.broker = broker + + if connect_only is None: + try: + connect_only = is_contains_context_name( + self.__class__.__name__, + TestApp.__name__, + ) + except Exception: # pragma: no cover + warnings.warn( + ( + "\nError `{e!r}` occurred at `{self.__class__.__name__}` AST parsing." + "\n`connect_only` is set to `False` by default." + ), + category=RuntimeWarning, + stacklevel=1, + ) + + connect_only = False + + self.connect_only = connect_only + self._fake_subscribers: list[SubscriberProto[Any]] = [] + + async def __aenter__(self) -> Broker: + self._ctx = self._create_ctx() + return await self._ctx.__aenter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> None: + await self._ctx.__aexit__(exc_type, exc_val, exc_tb) + + @asynccontextmanager + async def _create_ctx(self) -> AsyncGenerator[Broker, None]: + if self.with_real: + self._fake_start(self.broker) + context = FakeContext() + else: + context = self._patch_broker(self.broker) + + with context: + async with self.broker: + try: + if not self.connect_only: + await self.broker.start() + yield self.broker + finally: + self._fake_close(self.broker) + + @contextmanager + def _patch_producer(self, broker: Broker) -> Iterator[None]: + raise NotImplementedError + + @contextmanager + def _patch_logger(self, broker: Broker) -> Iterator[None]: + broker._setup_logger() + + logger_state = broker.config.logger + + old_log_object, logger_state.logger = ( + logger_state.logger, + RealLoggerObject(MagicMock()), + ) + + try: + yield + + finally: + logger_state.logger = old_log_object + + @contextmanager + def _patch_broker(self, broker: Broker) -> Generator[None, None, None]: + with ( + mock.patch.object( + broker, + "start", + wraps=partial(self._fake_start, broker), + ), + mock.patch.object( + broker, + "_connect", + wraps=partial(self._fake_connect, broker), + ), + mock.patch.object( + broker, + "close", + ), + mock.patch.object( + broker, + "_connection", + new=None, + ), + self._patch_producer(broker), + self._patch_logger(broker), + mock.patch.object( + broker, + "ping", + return_value=True, + ), + ): + yield + + def _fake_start(self, broker: Broker, *args: Any, **kwargs: Any) -> None: + for publisher in broker.publishers: + if getattr(publisher, "_fake_handler", None): + continue + + sub, is_real = self.create_publisher_fake_subscriber(broker, publisher) + + if not is_real: + self._fake_subscribers.append(sub) + + if not sub.calls: + + @sub + async def publisher_response_subscriber(msg: Any) -> None: + pass + + if is_real: + mock = MagicMock() + publisher.set_test(mock=mock, with_fake=False) # type: ignore[attr-defined] + for h in sub.calls: + h.handler.set_test() + assert h.handler.mock # nosec B101 + h.handler.mock.side_effect = mock + + else: + handler = sub.calls[0].handler + handler.set_test() + assert handler.mock # nosec B101 + publisher.set_test(mock=handler.mock, with_fake=True) # type: ignore[attr-defined] + + patch_broker_calls(broker) + + for subscriber in broker.subscribers: + subscriber._post_start() + + def _fake_close( + self, + broker: Broker, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> None: + for p in broker.publishers: + if getattr(p, "_fake_handler", None): + p.reset_test() # type: ignore[attr-defined] + + self.broker._subscribers = [ + sub for sub in self.broker._subscribers if sub not in self._fake_subscribers + ] + self._fake_subscribers.clear() + + for sub in broker.subscribers: + sub.running = False + for call in sub.calls: + call.handler.reset_test() + + @staticmethod + @abstractmethod + def create_publisher_fake_subscriber( + broker: Broker, + publisher: Any, + ) -> tuple["SubscriberProto[Any]", bool]: + raise NotImplementedError + + @staticmethod + @abstractmethod + async def _fake_connect(broker: Broker, *args: Any, **kwargs: Any) -> None: + raise NotImplementedError + + +def patch_broker_calls(broker: "BrokerUsecase[Any, Any]") -> None: + """Patch broker calls.""" + for sub in broker.subscribers: + sub._build_fastdepends_model() + + for h in sub.calls: + h.handler.set_test() diff --git a/faststream/_internal/types.py b/faststream/_internal/types.py new file mode 100644 index 0000000000..207bd485a8 --- /dev/null +++ b/faststream/_internal/types.py @@ -0,0 +1,88 @@ +from collections.abc import Awaitable, Callable +from typing import ( + TYPE_CHECKING, + Any, + Protocol, + TypeAlias, + TypeVar, +) + +from typing_extensions import ( + ParamSpec, + TypeVar as TypeVar313, +) + +from faststream._internal.basic_types import AsyncFuncAny +from faststream._internal.context.repository import ContextRepo +from faststream.message import StreamMessage +from faststream.response.response import PublishCommand + +if TYPE_CHECKING: + from faststream._internal.middlewares import BaseMiddleware + + +AnyMsg = TypeVar313("AnyMsg", default=Any) +AnyMsg_contra = TypeVar313("AnyMsg_contra", default=Any, contravariant=True) +MsgType = TypeVar("MsgType") +Msg_contra = TypeVar("Msg_contra", contravariant=True) +StreamMsg = TypeVar("StreamMsg", bound=StreamMessage[Any]) +ConnectionType = TypeVar("ConnectionType") +PublishCommandType = TypeVar313( + "PublishCommandType", + bound=PublishCommand, + default=Any, +) + +SyncFilter: TypeAlias = Callable[[StreamMsg], bool] +AsyncFilter: TypeAlias = Callable[[StreamMsg], Awaitable[bool]] +Filter: TypeAlias = SyncFilter[StreamMsg] | AsyncFilter[StreamMsg] + +SyncCallable: TypeAlias = Callable[ + [Any], + Any, +] +AsyncCallable: TypeAlias = AsyncFuncAny +AsyncCustomCallable: TypeAlias = AsyncFuncAny | Callable[[Any, AsyncFuncAny], Awaitable[Any]] +CustomCallable: TypeAlias = AsyncCustomCallable | SyncCallable + +P_HandlerParams = ParamSpec("P_HandlerParams") +T_HandlerReturn = TypeVar("T_HandlerReturn") + + +AsyncWrappedHandlerCall: TypeAlias = Callable[ + [StreamMessage[MsgType]], + Awaitable[T_HandlerReturn | None], +] +SyncWrappedHandlerCall: TypeAlias = Callable[ + [StreamMessage[MsgType]], + T_HandlerReturn | None, +] +WrappedHandlerCall: TypeAlias = AsyncWrappedHandlerCall[MsgType, T_HandlerReturn] | SyncWrappedHandlerCall[MsgType, T_HandlerReturn] + + +class BrokerMiddleware(Protocol[AnyMsg_contra, PublishCommandType]): + """Middleware builder interface.""" + + def __call__( + self, + msg: AnyMsg_contra | None, + /, + *, + context: ContextRepo, + ) -> "BaseMiddleware[PublishCommandType]": ... + + +SubscriberMiddleware: TypeAlias = Callable[ + [AsyncFuncAny, MsgType], + MsgType, +] + + +class PublisherMiddleware(Protocol): + """Publisher middleware interface.""" + + def __call__( + self, + call_next: Callable[[PublishCommand], Awaitable[Any]], + cmd: PublishCommand, + ) -> Any: ... diff --git a/faststream/_internal/utils/__init__.py b/faststream/_internal/utils/__init__.py new file mode 100644 index 0000000000..58684b7fdc --- /dev/null +++ b/faststream/_internal/utils/__init__.py @@ -0,0 +1,3 @@ +from fast_depends import inject as apply_types + +__all__ = ("apply_types",) diff --git a/faststream/_internal/utils/data.py b/faststream/_internal/utils/data.py new file mode 100644 index 0000000000..8f8a133636 --- /dev/null +++ b/faststream/_internal/utils/data.py @@ -0,0 +1,26 @@ +from typing import TypeVar + +from faststream._internal.basic_types import AnyDict + +TypedDictCls = TypeVar("TypedDictCls") + + +def filter_by_dict( + typed_dict: type[TypedDictCls], + data: AnyDict, +) -> tuple[TypedDictCls, AnyDict]: + annotations = typed_dict.__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), # type: ignore[call-arg] + extra_data, + ) diff --git a/faststream/_internal/utils/functions.py b/faststream/_internal/utils/functions.py new file mode 100644 index 0000000000..5c55e2fb66 --- /dev/null +++ b/faststream/_internal/utils/functions.py @@ -0,0 +1,118 @@ +import asyncio +from collections.abc import AsyncIterator, Awaitable, Callable +from concurrent.futures import Executor +from contextlib import asynccontextmanager +from functools import partial, wraps +from typing import ( + TYPE_CHECKING, + Any, + Optional, + TypeVar, + cast, + overload, +) + +from fast_depends.core import CallModel +from fast_depends.utils import ( + is_coroutine_callable, + run_async as call_or_await, + run_in_threadpool, +) +from typing_extensions import ParamSpec, Self + +from faststream._internal.basic_types import F_Return, F_Spec + +if TYPE_CHECKING: + from types import TracebackType + +__all__ = ( + "call_or_await", + "drop_response_type", + "fake_context", + "to_async", +) + +P = ParamSpec("P") +T = TypeVar("T") + + +@overload +def to_async( + func: Callable[F_Spec, Awaitable[F_Return]], +) -> Callable[F_Spec, Awaitable[F_Return]]: ... + + +@overload +def to_async( + func: Callable[F_Spec, F_Return], +) -> Callable[F_Spec, Awaitable[F_Return]]: ... + + +def to_async( + func: Callable[F_Spec, F_Return] | Callable[F_Spec, Awaitable[F_Return]], +) -> Callable[F_Spec, Awaitable[F_Return]]: + """Converts a synchronous function to an asynchronous function.""" + if is_coroutine_callable(func): + return cast("Callable[F_Spec, Awaitable[F_Return]]", func) + + func = cast("Callable[F_Spec, F_Return]", func) + + @wraps(func) + async def to_async_wrapper(*args: F_Spec.args, **kwargs: F_Spec.kwargs) -> F_Return: + """Wraps a function to make it asynchronous.""" + return await run_in_threadpool(func, *args, **kwargs) + + return to_async_wrapper + + +@asynccontextmanager +async def fake_context(*args: Any, **kwargs: Any) -> AsyncIterator[None]: + yield None + + +class FakeContext: + def __init__(self, *args: Any, **kwargs: Any) -> None: + pass + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> None: + if exc_val: + raise exc_val + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> None: + if exc_val: + raise exc_val + + +def drop_response_type(model: CallModel) -> CallModel: + setattr(model.serializer, "response_callback", None) # noqa: B010 + return model + + +async def return_input(x: Any) -> Any: + return x + + +async def run_in_executor( + executor: Executor | None, + func: Callable[P, T], + *args: P.args, + **kwargs: P.kwargs, +) -> T: + loop = asyncio.get_running_loop() + return await loop.run_in_executor(executor, partial(func, *args, **kwargs)) diff --git a/faststream/utils/nuid.py b/faststream/_internal/utils/nuid.py similarity index 96% rename from faststream/utils/nuid.py rename to faststream/_internal/utils/nuid.py index a61aa08a8f..b15dc92ac1 100644 --- a/faststream/utils/nuid.py +++ b/faststream/_internal/utils/nuid.py @@ -36,7 +36,7 @@ class NUID: """ def __init__(self) -> None: - self._prand = Random(randbelow(max_int)) # nosec B311 + self._prand = Random(randbelow(max_int)) # nosec B311 # noqa: S311 self._seq = self._prand.randint(0, MAX_SEQ) self._inc = MIN_INC + self._prand.randint(BASE + 1, INC) self._prefix = bytearray() diff --git a/faststream/utils/path.py b/faststream/_internal/utils/path.py similarity index 81% rename from faststream/utils/path.py rename to faststream/_internal/utils/path.py index c3c059bf71..58d126bad1 100644 --- a/faststream/utils/path.py +++ b/faststream/_internal/utils/path.py @@ -1,16 +1,17 @@ import re -from typing import Callable, Optional, Pattern, Tuple +from collections.abc import Callable +from re import Pattern from faststream.exceptions import SetupError -PARAM_REGEX = re.compile("{([a-zA-Z0-9_]+)}") +PARAM_REGEX = re.compile(r"{([a-zA-Z0-9_]+)}") def compile_path( path: str, replace_symbol: str, patch_regex: Callable[[str], str] = lambda x: x, -) -> Tuple[Optional[Pattern[str]], str]: +) -> tuple[Pattern[str] | None, str]: path_regex = "^.*?" original_path = "" @@ -36,7 +37,8 @@ def compile_path( if duplicated_params: names = ", ".join(sorted(duplicated_params)) ending = "s" if len(duplicated_params) > 1 else "" - raise SetupError(f"Duplicated param name{ending} {names} at path {path}") + msg = f"Duplicated param name{ending} {names} at path {path}" + raise SetupError(msg) if idx == 0: regex = None diff --git a/faststream/annotations.py b/faststream/annotations.py index 5532daf9f5..a845df0471 100644 --- a/faststream/annotations.py +++ b/faststream/annotations.py @@ -1,16 +1,10 @@ import logging -from typing import TypeVar - -from typing_extensions import Annotated +from typing import Annotated +from faststream._internal.context import ContextRepo as CR from faststream.app import FastStream as FS -from faststream.utils.context import Context -from faststream.utils.context import ContextRepo as CR -from faststream.utils.no_cast import NoCast as NC - -_NoCastType = TypeVar("_NoCastType") +from faststream.params import Context Logger = Annotated[logging.Logger, Context("logger")] ContextRepo = Annotated[CR, Context("context")] -NoCast = Annotated[_NoCastType, NC()] FastStream = Annotated[FS, Context("app")] diff --git a/faststream/app.py b/faststream/app.py index 67d9be7600..b0405cdfc9 100644 --- a/faststream/app.py +++ b/faststream/app.py @@ -1,79 +1,102 @@ import logging +from collections.abc import Sequence from typing import ( TYPE_CHECKING, - AsyncIterator, - Dict, + Any, Optional, - Sequence, - Tuple, TypeVar, ) import anyio +from fast_depends import Provider from typing_extensions import ParamSpec -from faststream._compat import ExceptionGroup +from faststream._internal._compat import ExceptionGroup from faststream._internal.application import Application +from faststream._internal.basic_types import Lifespan, LoggerProto +from faststream._internal.cli.supervisors.utils import set_exit +from faststream._internal.constants import EMPTY +from faststream._internal.di import FastDependsConfig +from faststream._internal.logger import logger from faststream.asgi.app import AsgiFastStream -from faststream.cli.supervisors.utils import set_exit -from faststream.exceptions import ValidationError - -P_HookParams = ParamSpec("P_HookParams") -T_HookReturn = TypeVar("T_HookReturn") - if TYPE_CHECKING: + from fast_depends.library.serializer import SerializerProto + + from faststream._internal.basic_types import ( + AnyCallable, + Lifespan, + LoggerProto, + SettingField, + ) + from faststream._internal.broker import BrokerUsecase from faststream.asgi.types import ASGIApp - from faststream.types import SettingField + +P_HookParams = ParamSpec("P_HookParams") +T_HookReturn = TypeVar("T_HookReturn") class FastStream(Application): """A class representing a FastStream application.""" + def __init__( + self, + broker: Optional["BrokerUsecase[Any, Any]"] = None, + /, + logger: Optional["LoggerProto"] = logger, + provider: Optional["Provider"] = None, + serializer: Optional["SerializerProto"] = EMPTY, + lifespan: Optional["Lifespan"] = None, + on_startup: Sequence["AnyCallable"] = (), + after_startup: Sequence["AnyCallable"] = (), + on_shutdown: Sequence["AnyCallable"] = (), + after_shutdown: Sequence["AnyCallable"] = (), + ) -> None: + super().__init__( + broker, + logger=logger, + config=FastDependsConfig( + provider=provider or Provider(), + serializer=serializer, + ), + lifespan=lifespan, + on_startup=on_startup, + after_startup=after_startup, + on_shutdown=on_shutdown, + after_shutdown=after_shutdown, + ) + + self._should_exit = False + async def run( self, log_level: int = logging.INFO, - run_extra_options: Optional[Dict[str, "SettingField"]] = None, + run_extra_options: dict[str, "SettingField"] | None = None, sleep_time: float = 0.1, ) -> None: """Run FastStream Application.""" set_exit(lambda *_: self.exit(), sync=False) - async with catch_startup_validation_error(), self.lifespan_context( - **(run_extra_options or {}) - ): + async with self.lifespan_context(**(run_extra_options or {})): try: async with anyio.create_task_group() as tg: tg.start_soon(self._startup, log_level, run_extra_options) - await self._main_loop(sleep_time) + + while not self._should_exit: # noqa: ASYNC110 (requested by creator) + await anyio.sleep(sleep_time) + await self._shutdown(log_level) tg.cancel_scope.cancel() except ExceptionGroup as e: for ex in e.exceptions: raise ex from None + def exit(self) -> None: + """Stop application manually.""" + self._should_exit = True + def as_asgi( self, - asgi_routes: Sequence[Tuple[str, "ASGIApp"]] = (), - asyncapi_path: Optional[str] = None, + asgi_routes: Sequence[tuple[str, "ASGIApp"]] = (), ) -> AsgiFastStream: - return AsgiFastStream.from_app(self, asgi_routes, asyncapi_path) - - -try: - from contextlib import asynccontextmanager - - from pydantic import ValidationError as PValidation - - @asynccontextmanager - async def catch_startup_validation_error() -> AsyncIterator[None]: - try: - yield - except PValidation as e: - fields = [str(x["loc"][0]) for x in e.errors()] - raise ValidationError(fields=fields) from e - -except ImportError: - from faststream.utils.functions import fake_context - - catch_startup_validation_error = fake_context + return AsgiFastStream.from_app(self, asgi_routes) diff --git a/faststream/asgi/app.py b/faststream/asgi/app.py index 6514a48ca7..32bdfa81c6 100644 --- a/faststream/asgi/app.py +++ b/faststream/asgi/app.py @@ -1,54 +1,78 @@ import inspect import logging import traceback +from abc import abstractmethod +from collections.abc import AsyncIterator, Sequence from contextlib import asynccontextmanager -from typing import ( - TYPE_CHECKING, - Any, - AsyncIterator, - Dict, - Optional, - Sequence, - Tuple, - Union, -) +from typing import TYPE_CHECKING, Any, Optional, Protocol import anyio -from faststream._compat import HAS_UVICORN, uvicorn +from faststream._internal._compat import HAS_TYPER, HAS_UVICORN, ExceptionGroup, uvicorn from faststream._internal.application import Application -from faststream.exceptions import INSTALL_UVICORN -from faststream.log.logging import logger +from faststream._internal.constants import EMPTY +from faststream._internal.di import FastDependsConfig +from faststream._internal.logger import logger +from faststream.exceptions import INSTALL_UVICORN, StartupValidationError -from .factories import make_asyncapi_asgi from .response import AsgiResponse from .websocket import WebSocketClose if TYPE_CHECKING: - from faststream.asyncapi.schema import ( - Contact, - ContactDict, - ExternalDocs, - ExternalDocsDict, - License, - LicenseDict, - Tag, - TagDict, - ) - from faststream.broker.core.usecase import BrokerUsecase - from faststream.types import ( + from types import FrameType + + from anyio.abc import TaskStatus + from fast_depends import Provider + from fast_depends.library.serializer import SerializerProto + + from faststream._internal.basic_types import ( AnyCallable, AnyDict, - AnyHttpUrl, Lifespan, LoggerProto, SettingField, ) + from faststream._internal.broker import BrokerUsecase + + class UvicornServerProtocol(Protocol): + should_exit: bool + force_exit: bool + + def handle_exit(self, sig: int, frame: FrameType | None) -> None: ... from .types import ASGIApp, Receive, Scope, Send -def cast_uvicorn_params(params: Dict[str, Any]) -> Dict[str, Any]: +class ServerState(Protocol): + extra_options: dict[str, "SettingField"] + + @abstractmethod + def stop(self) -> None: ... + + +class OuterRunState(ServerState): + def __init__(self) -> None: + self.extra_options = {} + + def stop(self) -> None: + # TODO: resend signal to outer uvicorn + pass + + +class CliRunState(ServerState): + def __init__( + self, + server: "UvicornServerProtocol", + extra_options: dict[str, "SettingField"], + ) -> None: + self.server = server + self.extra_options = extra_options + + def stop(self) -> None: + self.server.should_exit = True + + +def cast_uvicorn_params(params: "AnyDict") -> "AnyDict": if port := params.get("port"): params["port"] = int(port) if fd := params.get("fd"): @@ -57,45 +81,32 @@ def cast_uvicorn_params(params: Dict[str, Any]) -> Dict[str, Any]: class AsgiFastStream(Application): + _server: ServerState + def __init__( self, broker: Optional["BrokerUsecase[Any, Any]"] = None, /, - asgi_routes: Sequence[Tuple[str, "ASGIApp"]] = (), - asyncapi_path: Optional[str] = None, + asgi_routes: Sequence[tuple[str, "ASGIApp"]] = (), # regular broker args logger: Optional["LoggerProto"] = logger, + provider: Optional["Provider"] = None, + serializer: Optional["SerializerProto"] = EMPTY, lifespan: Optional["Lifespan"] = None, - # AsyncAPI args, - title: str = "FastStream", - version: str = "0.1.0", - description: str = "", - 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, - external_docs: Optional[ - Union["ExternalDocs", "ExternalDocsDict", "AnyDict"] - ] = None, - identifier: Optional[str] = None, + # hooks on_startup: Sequence["AnyCallable"] = (), after_startup: Sequence["AnyCallable"] = (), on_shutdown: Sequence["AnyCallable"] = (), after_shutdown: Sequence["AnyCallable"] = (), ) -> None: super().__init__( - broker=broker, + broker, logger=logger, + config=FastDependsConfig( + provider=provider, + serializer=serializer, + ), lifespan=lifespan, - title=title, - version=version, - description=description, - terms_of_service=terms_of_service, - license=license, - contact=contact, - tags=tags, - external_docs=external_docs, - identifier=identifier, on_startup=on_startup, after_startup=after_startup, on_shutdown=on_shutdown, @@ -103,34 +114,23 @@ def __init__( ) self.routes = list(asgi_routes) - if asyncapi_path: - self.mount(asyncapi_path, make_asyncapi_asgi(self)) + + self._server = OuterRunState() self._log_level: int = logging.INFO - self._run_extra_options: Dict[str, SettingField] = {} + self._run_extra_options: dict[str, SettingField] = {} @classmethod def from_app( cls, app: Application, - asgi_routes: Sequence[Tuple[str, "ASGIApp"]], - asyncapi_path: Optional[str] = None, + asgi_routes: Sequence[tuple[str, "ASGIApp"]], ) -> "AsgiFastStream": asgi_app = cls( app.broker, asgi_routes=asgi_routes, - asyncapi_path=asyncapi_path, logger=app.logger, lifespan=None, - title=app.title, - version=app.version, - description=app.description, - terms_of_service=app.terms_of_service, - license=app.license, - contact=app.contact, - tags=app.asyncapi_tags, - external_docs=app.external_docs, - identifier=app.identifier, ) asgi_app.lifespan_context = app.lifespan_context asgi_app._on_startup_calling = app._on_startup_calling @@ -142,7 +142,13 @@ def from_app( def mount(self, path: str, route: "ASGIApp") -> None: self.routes.append((path, route)) - async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: + async def __call__( + self, + scope: "Scope", + receive: "Receive", + send: "Send", + ) -> None: + """ASGI implementation.""" if scope["type"] == "lifespan": await self.lifespan(scope, receive, send) return @@ -159,8 +165,7 @@ async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> No async def run( self, log_level: int = logging.INFO, - run_extra_options: Optional[Dict[str, "SettingField"]] = None, - sleep_time: float = 0.1, + run_extra_options: dict[str, "SettingField"] | None = None, ) -> None: if not HAS_UVICORN: raise ImportError(INSTALL_UVICORN) @@ -181,41 +186,88 @@ async def run( server = uvicorn.Server(config) await server.serve() + def exit(self) -> None: + """Manual stop method.""" + self._server.stop() + @asynccontextmanager - async def start_lifespan_context(self) -> AsyncIterator[None]: - async with anyio.create_task_group() as tg, self.lifespan_context(): - tg.start_soon(self._startup, self._log_level, self._run_extra_options) + async def start_lifespan_context( + self, + run_extra_options: dict[str, "SettingField"] | None = None, + ) -> AsyncIterator[None]: + run_extra_options = run_extra_options or self._run_extra_options + async with self.lifespan_context(**run_extra_options): try: - yield - finally: - await self._shutdown() - tg.cancel_scope.cancel() + async with anyio.create_task_group() as tg: + await tg.start(self.__start, logging.INFO, run_extra_options) + + try: + yield + finally: + await self._shutdown() + tg.cancel_scope.cancel() + + except ExceptionGroup as e: + for ex in e.exceptions: + raise ex from None + + async def __start( + self, + log_level: int, + run_extra_options: dict[str, "SettingField"], + *, + task_status: "TaskStatus[None]" = anyio.TASK_STATUS_IGNORED, + ) -> None: + """Redefenition of `_startup` method. + + Waits for hooks run before broker start. + """ + async with ( + self._startup_logging(log_level=log_level), + self._start_hooks_context(**run_extra_options), + ): + task_status.started() + await self._start_broker() async def lifespan(self, scope: "Scope", receive: "Receive", send: "Send") -> None: """Handle ASGI lifespan messages to start and shutdown the app.""" started = False await receive() # handle `lifespan.startup` event + async def process_exception(ex: BaseException) -> None: + exc_text = traceback.format_exc() + if started: + await send({"type": "lifespan.shutdown.failed", "message": exc_text}) + else: + await send({"type": "lifespan.startup.failed", "message": exc_text}) + raise ex + try: async with self.start_lifespan_context(): await send({"type": "lifespan.startup.complete"}) started = True await receive() # handle `lifespan.shutdown` event - except BaseException: - exc_text = traceback.format_exc() - if started: - await send({"type": "lifespan.shutdown.failed", "message": exc_text}) + except StartupValidationError as startup_exc: + # Process `on_startup` and `lifespan` missed extra options + if HAS_TYPER: + from faststream._internal.cli.utils.errors import draw_startup_errors + + draw_startup_errors(startup_exc) + await send({"type": "lifespan.startup.failed", "message": ""}) + else: - await send({"type": "lifespan.startup.failed", "message": exc_text}) - raise + await process_exception(startup_exc) + + except BaseException as base_exc: + await process_exception(base_exc) else: await send({"type": "lifespan.shutdown.complete"}) async def not_found(self, scope: "Scope", receive: "Receive", send: "Send") -> None: - not_found_msg = "App doesn't support regular HTTP protocol." + not_found_msg = "Application doesn't support regular HTTP protocol." if scope["type"] == "websocket": websocket_close = WebSocketClose( diff --git a/faststream/asgi/factories.py b/faststream/asgi/factories.py index bfb33f56e2..87cfaa937e 100644 --- a/faststream/asgi/factories.py +++ b/faststream/asgi/factories.py @@ -1,13 +1,7 @@ -from typing import ( - TYPE_CHECKING, - Any, - Optional, - Sequence, - Union, -) +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Union -from faststream.asyncapi import get_app_schema -from faststream.asyncapi.site import ( +from faststream.specification.asyncapi.site import ( ASYNCAPI_CSS_DEFAULT_URL, ASYNCAPI_JS_DEFAULT_URL, get_asyncapi_html, @@ -17,10 +11,10 @@ from .response import AsgiResponse if TYPE_CHECKING: - from faststream.asyncapi.proto import AsyncAPIApplication - from faststream.asyncapi.schema import Tag, TagDict - from faststream.broker.core.usecase import BrokerUsecase - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict + from faststream._internal.broker import BrokerUsecase + from faststream.specification import Specification + from faststream.specification.schema import Tag, TagDict from .types import ASGIApp, Scope @@ -28,11 +22,11 @@ def make_ping_asgi( broker: "BrokerUsecase[Any, Any]", /, - timeout: Optional[float] = None, + timeout: float | None = None, include_in_schema: bool = True, - description: Optional[str] = None, - tags: Optional[Sequence[Union["Tag", "TagDict", "AnyDict"]]] = None, - unique_id: Optional[str] = None, + description: str | None = None, + tags: Sequence[Union["Tag", "TagDict", "AnyDict"]] | None = None, + unique_id: str | None = None, ) -> "ASGIApp": healthy_response = AsgiResponse(b"", 204) unhealthy_response = AsgiResponse(b"", 500) @@ -52,7 +46,7 @@ async def ping(scope: "Scope") -> AsgiResponse: def make_asyncapi_asgi( - app: "AsyncAPIApplication", + schema: "Specification", sidebar: bool = True, info: bool = True, servers: bool = True, @@ -60,14 +54,13 @@ def make_asyncapi_asgi( messages: bool = True, schemas: bool = True, errors: bool = True, + include_in_schema: bool = True, expand_message_examples: bool = True, - title: str = "FastStream", asyncapi_js_url: str = ASYNCAPI_JS_DEFAULT_URL, asyncapi_css_url: str = ASYNCAPI_CSS_DEFAULT_URL, - include_in_schema: bool = True, - description: Optional[str] = None, - tags: Optional[Sequence[Union["Tag", "TagDict", "AnyDict"]]] = None, - unique_id: Optional[str] = None, + description: str | None = None, + tags: Sequence[Union["Tag", "TagDict", "AnyDict"]] | None = None, + unique_id: str | None = None, ) -> "ASGIApp": cached_docs = None @@ -81,7 +74,7 @@ async def docs(scope: "Scope") -> AsgiResponse: nonlocal cached_docs if not cached_docs: cached_docs = get_asyncapi_html( - get_app_schema(app), + schema.schema, sidebar=sidebar, info=info, servers=servers, @@ -90,7 +83,6 @@ async def docs(scope: "Scope") -> AsgiResponse: schemas=schemas, errors=errors, expand_message_examples=expand_message_examples, - title=title, asyncapi_js_url=asyncapi_js_url, asyncapi_css_url=asyncapi_css_url, ) diff --git a/faststream/asgi/handlers.py b/faststream/asgi/handlers.py index f441caa601..e03500c62c 100644 --- a/faststream/asgi/handlers.py +++ b/faststream/asgi/handlers.py @@ -1,17 +1,11 @@ -from typing import ( - TYPE_CHECKING, - Callable, - Optional, - Sequence, - Union, - overload, -) +from collections.abc import Callable, Sequence +from typing import TYPE_CHECKING, Optional, Union, overload from .response import AsgiResponse if TYPE_CHECKING: - from faststream.asyncapi.schema import Tag, TagDict - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict + from faststream.specification.schema import Tag, TagDict from .types import ASGIApp, Receive, Scope, Send, UserApp @@ -22,11 +16,11 @@ def __init__( func: "UserApp", *, include_in_schema: bool = True, - description: Optional[str] = None, - methods: Optional[Sequence[str]] = None, - tags: Optional[Sequence[Union["Tag", "TagDict", "AnyDict"]]] = None, - unique_id: Optional[str] = None, - ): + description: str | None = None, + methods: Sequence[str] | None = None, + tags: Sequence[Union["Tag", "TagDict", "AnyDict"]] | None = None, + unique_id: str | None = None, + ) -> None: self.func = func self.methods = methods or () self.include_in_schema = include_in_schema @@ -45,7 +39,6 @@ async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> No response = AsgiResponse(body=b"Internal Server Error", status_code=500) await response(scope, receive, send) - return class GetHandler(HttpHandler): @@ -54,10 +47,10 @@ def __init__( func: "UserApp", *, include_in_schema: bool = True, - description: Optional[str] = None, - tags: Optional[Sequence[Union["Tag", "TagDict", "AnyDict"]]] = None, - unique_id: Optional[str] = None, - ): + description: str | None = None, + tags: Sequence[Union["Tag", "TagDict", "AnyDict"]] | None = None, + unique_id: str | None = None, + ) -> None: super().__init__( func, include_in_schema=include_in_schema, @@ -73,9 +66,9 @@ def get( func: "UserApp", *, include_in_schema: bool = True, - description: Optional[str] = None, - tags: Optional[Sequence[Union["Tag", "TagDict", "AnyDict"]]] = None, - unique_id: Optional[str] = None, + description: str | None = None, + tags: Sequence[Union["Tag", "TagDict", "AnyDict"]] | None = None, + unique_id: str | None = None, ) -> "ASGIApp": ... @@ -84,9 +77,9 @@ def get( func: None = None, *, include_in_schema: bool = True, - description: Optional[str] = None, - tags: Optional[Sequence[Union["Tag", "TagDict", "AnyDict"]]] = None, - unique_id: Optional[str] = None, + description: str | None = None, + tags: Sequence[Union["Tag", "TagDict", "AnyDict"]] | None = None, + unique_id: str | None = None, ) -> Callable[["UserApp"], "ASGIApp"]: ... @@ -94,9 +87,9 @@ def get( func: Optional["UserApp"] = None, *, include_in_schema: bool = True, - description: Optional[str] = None, - tags: Optional[Sequence[Union["Tag", "TagDict", "AnyDict"]]] = None, - unique_id: Optional[str] = None, + description: str | None = None, + tags: Sequence[Union["Tag", "TagDict", "AnyDict"]] | None = None, + unique_id: str | None = None, ) -> Union[Callable[["UserApp"], "ASGIApp"], "ASGIApp"]: def decorator(inner_func: "UserApp") -> "ASGIApp": return GetHandler( diff --git a/faststream/asgi/response.py b/faststream/asgi/response.py index 790a2c99be..32c287b1ae 100644 --- a/faststream/asgi/response.py +++ b/faststream/asgi/response.py @@ -1,4 +1,5 @@ -from typing import TYPE_CHECKING, List, Mapping, Optional, Tuple +from collections.abc import Mapping +from typing import TYPE_CHECKING if TYPE_CHECKING: from .types import Receive, Scope, Send @@ -7,9 +8,9 @@ class AsgiResponse: def __init__( self, - body: bytes, - status_code: int, - headers: Optional[Mapping[str, str]] = None, + body: bytes = b"", + status_code: int = 200, + headers: Mapping[str, str] | None = None, ) -> None: self.status_code = status_code self.body = body @@ -22,23 +23,23 @@ async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> No "type": f"{prefix}http.response.start", "status": self.status_code, "headers": self.raw_headers, - } + }, ) await send( { "type": f"{prefix}http.response.body", "body": self.body, - } + }, ) def _get_response_headers( body: bytes, - headers: Optional[Mapping[str, str]], + headers: Mapping[str, str] | None, status_code: int, -) -> List[Tuple[bytes, bytes]]: +) -> list[tuple[bytes, bytes]]: if headers is None: - raw_headers: List[Tuple[bytes, bytes]] = [] + raw_headers: list[tuple[bytes, bytes]] = [] populate_content_length = True else: @@ -52,7 +53,7 @@ def _get_response_headers( if ( body and populate_content_length - and not (status_code < 200 or status_code in (204, 304)) + and not (status_code < 200 or status_code in {204, 304}) ): content_length = str(len(body)) raw_headers.append((b"content-length", content_length.encode("latin-1"))) diff --git a/faststream/asgi/types.py b/faststream/asgi/types.py index df9d96b098..d64c718821 100644 --- a/faststream/asgi/types.py +++ b/faststream/asgi/types.py @@ -1,4 +1,5 @@ -from typing import Any, Awaitable, Callable, MutableMapping +from collections.abc import Awaitable, Callable, MutableMapping +from typing import Any Scope = MutableMapping[str, Any] Message = MutableMapping[str, Any] diff --git a/faststream/asgi/websocket.py b/faststream/asgi/websocket.py index 0e44fefd49..5345a28d6a 100644 --- a/faststream/asgi/websocket.py +++ b/faststream/asgi/websocket.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: from .types import Receive, Scope, Send @@ -8,12 +8,12 @@ class WebSocketClose: def __init__( self, code: int, - reason: Optional[str], + reason: str | None, ) -> None: self.code = code self.reason = reason or "" async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: await send( - {"type": "websocket.close", "code": self.code, "reason": self.reason} + {"type": "websocket.close", "code": self.code, "reason": self.reason}, ) diff --git a/faststream/asyncapi/__init__.py b/faststream/asyncapi/__init__.py deleted file mode 100644 index be11a98029..0000000000 --- a/faststream/asyncapi/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""AsyncAPI related functions.""" - -from faststream.asyncapi.generate import get_app_schema -from faststream.asyncapi.site import get_asyncapi_html - -__all__ = ( - "get_app_schema", - "get_asyncapi_html", -) diff --git a/faststream/asyncapi/abc.py b/faststream/asyncapi/abc.py deleted file mode 100644 index 64dd0fccde..0000000000 --- a/faststream/asyncapi/abc.py +++ /dev/null @@ -1,45 +0,0 @@ -from abc import abstractmethod -from typing import Any, Dict, Optional - -from faststream.asyncapi.proto import AsyncAPIProto -from faststream.asyncapi.schema.channels import Channel - - -class AsyncAPIOperation(AsyncAPIProto): - """A class representing an asynchronous API operation.""" - - @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() - else: - 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/asyncapi/generate.py b/faststream/asyncapi/generate.py deleted file mode 100644 index 9e124e3105..0000000000 --- a/faststream/asyncapi/generate.py +++ /dev/null @@ -1,262 +0,0 @@ -from typing import TYPE_CHECKING, Any, Dict, List - -from faststream._compat import DEF_KEY -from faststream.asyncapi.schema import ( - Channel, - Components, - Info, - Message, - Operation, - OperationBinding, - Reference, - Schema, - Server, -) -from faststream.asyncapi.schema.bindings import http as http_bindings -from faststream.constants import ContentTypes - -if TYPE_CHECKING: - from faststream.asyncapi.proto import AsyncAPIApplication - from faststream.broker.core.usecase import BrokerUsecase - from faststream.broker.types import ConnectionType, MsgType - - -def get_app_schema(app: "AsyncAPIApplication") -> Schema: - """Get the application schema.""" - broker = app.broker - if broker is None: # pragma: no cover - raise RuntimeError() - broker.setup() - - servers = get_broker_server(broker) - - channels = get_broker_channels(broker) - for ch in channels.values(): - ch.servers = list(servers.keys()) - - channels.update(get_asgi_routes(app)) - - messages: Dict[str, Message] = {} - payloads: Dict[str, Dict[str, Any]] = {} - for channel_name, ch in channels.items(): - if ch.subscribe is not None: - m = ch.subscribe.message - - if isinstance(m, Message): # pragma: no branch - ch.subscribe.message = _resolve_msg_payloads( - m, - channel_name, - payloads, - messages, - ) - - if ch.publish is not None: - m = ch.publish.message - - if isinstance(m, Message): # pragma: no branch - ch.publish.message = _resolve_msg_payloads( - m, - channel_name, - payloads, - messages, - ) - - schema = Schema( - info=Info( - title=app.title, - version=app.version, - description=app.description, - termsOfService=app.terms_of_service, - contact=app.contact, - license=app.license, - ), - defaultContentType=ContentTypes.json.value, - id=app.identifier, - tags=list(app.asyncapi_tags) if app.asyncapi_tags else None, - externalDocs=app.external_docs, - servers=servers, - channels=channels, - components=Components( - messages=messages, - schemas=payloads, - securitySchemes=None - if broker.security is None - else broker.security.get_schema(), - ), - ) - return schema - - -def get_broker_server( - broker: "BrokerUsecase[MsgType, ConnectionType]", -) -> Dict[str, Server]: - """Get the broker server for an application.""" - servers = {} - - broker_meta: Dict[str, Any] = { - "protocol": broker.protocol, - "protocolVersion": broker.protocol_version, - "description": broker.description, - "tags": broker.tags, - # TODO - # "variables": "", - # "bindings": "", - } - - if broker.security is not None: - broker_meta["security"] = broker.security.get_requirement() - - if isinstance(broker.url, str): - servers["development"] = Server( - url=broker.url, - **broker_meta, - ) - - elif len(broker.url) == 1: - servers["development"] = Server( - url=broker.url[0], - **broker_meta, - ) - - else: - for i, url in enumerate(broker.url, 1): - servers[f"Server{i}"] = Server( - url=url, - **broker_meta, - ) - - return servers - - -def get_broker_channels( - broker: "BrokerUsecase[MsgType, ConnectionType]", -) -> Dict[str, Channel]: - """Get the broker channels for an application.""" - channels = {} - - for h in broker._subscribers.values(): - channels.update(h.schema()) - - for p in broker._publishers.values(): - channels.update(p.schema()) - - return channels - - -def get_asgi_routes(app: "AsyncAPIApplication") -> Dict[str, Channel]: - """Get the ASGI routes for an application.""" - # We should import this here due - # ASGI > Application > asynciapi.proto - # so it looks like a circular import - from faststream.asgi import AsgiFastStream - from faststream.asgi.handlers import HttpHandler - - if not isinstance(app, AsgiFastStream): - return {} - - channels: Dict[str, Channel] = {} - for route in app.routes: - path, asgi_app = route - - if isinstance(asgi_app, HttpHandler) and asgi_app.include_in_schema: - channel = Channel( - description=asgi_app.description, - subscribe=Operation( - tags=asgi_app.tags, - operationId=asgi_app.unique_id, - bindings=OperationBinding( - http=http_bindings.OperationBinding( - method=", ".join(asgi_app.methods) - ) - ), - ), - ) - - channels[path] = channel - - return channels - - -def _resolve_msg_payloads( - m: Message, - channel_name: str, - payloads: Dict[str, Any], - messages: Dict[str, Any], -) -> Reference: - """Replace message payload by reference and normalize payloads. - - Payloads and messages are editable dicts to store schemas for reference in AsyncAPI. - """ - one_of_list: List[Reference] = [] - m.payload = _move_pydantic_refs(m.payload, DEF_KEY) - - if DEF_KEY in m.payload: - payloads.update(m.payload.pop(DEF_KEY)) - - one_of = m.payload.get("oneOf") - if isinstance(one_of, dict): - for p_title, p in one_of.items(): - p_title = p_title.replace("/", ".") - payloads.update(p.pop(DEF_KEY, {})) - if p_title not in payloads: - payloads[p_title] = p - one_of_list.append(Reference(**{"$ref": f"#/components/schemas/{p_title}"})) - - elif one_of is not None: - # Descriminator case - for p in one_of: - p_value = next(iter(p.values())) - p_title = p_value.split("/")[-1] - p_title = p_title.replace("/", ".") - if p_title not in payloads: - payloads[p_title] = p - one_of_list.append(Reference(**{"$ref": f"#/components/schemas/{p_title}"})) - - if not one_of_list: - payloads.update(m.payload.pop(DEF_KEY, {})) - p_title = m.payload.get("title", f"{channel_name}Payload") - p_title = p_title.replace("/", ".") - if p_title not in payloads: - payloads[p_title] = m.payload - m.payload = {"$ref": f"#/components/schemas/{p_title}"} - - else: - m.payload["oneOf"] = one_of_list - - assert m.title # nosec B101 - m.title = m.title.replace("/", ".") - messages[m.title] = m - return Reference(**{"$ref": f"#/components/messages/{m.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/asyncapi/message.py b/faststream/asyncapi/message.py deleted file mode 100644 index 149c5e3bf1..0000000000 --- a/faststream/asyncapi/message.py +++ /dev/null @@ -1,138 +0,0 @@ -from inspect import isclass -from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Type, overload - -from pydantic import BaseModel, create_model - -from faststream._compat import DEF_KEY, PYDANTIC_V2, get_model_fields, model_schema - -if TYPE_CHECKING: - from fast_depends.core import CallModel - - -def parse_handler_params( - call: "CallModel[Any, Any]", prefix: str = "" -) -> Dict[str, Any]: - """Parses the handler parameters.""" - model = call.model - assert model # nosec B101 - - body = get_model_schema( - create_model( # type: ignore[call-overload] - model.__name__, - **call.flat_params, - ), - prefix=prefix, - exclude=tuple(call.custom_fields.keys()), - ) - - if body is None: - return {"title": "EmptyPayload", "type": "null"} - - return body - - -@overload -def get_response_schema(call: None, prefix: str = "") -> None: ... - - -@overload -def get_response_schema( - call: "CallModel[Any, Any]", prefix: str = "" -) -> Dict[str, Any]: ... - - -def get_response_schema( - call: Optional["CallModel[Any, Any]"], - prefix: str = "", -) -> Optional[Dict[str, Any]]: - """Get the response schema for a given call.""" - return get_model_schema( - getattr( - call, "response_model", None - ), # NOTE: FastAPI Dependant object compatibility - prefix=prefix, - ) - - -@overload -def get_model_schema( - call: None, - prefix: str = "", - exclude: Sequence[str] = (), -) -> None: ... - - -@overload -def get_model_schema( - call: Type[BaseModel], - prefix: str = "", - exclude: Sequence[str] = (), -) -> Dict[str, Any]: ... - - -def get_model_schema( - call: Optional[Type[BaseModel]], - prefix: str = "", - exclude: Sequence[str] = (), -) -> Optional[Dict[str, Any]]: - """Get the schema of a model.""" - if call is None: - return None - - params = {k: v for k, v in get_model_fields(call).items() if k not in exclude} - params_number = len(params) - - if params_number == 0: - return None - - model = None - use_original_model = False - if params_number == 1: - name, param = next(iter(params.items())) - if ( - param.annotation - and isclass(param.annotation) - and issubclass(param.annotation, BaseModel) # NOTE: 3.7-3.10 compatibility - ): - model = param.annotation - use_original_model = True - - if model is None: - model = call - - body: Dict[str, Any] = model_schema(model) - body["properties"] = body.get("properties", {}) - for i in exclude: - body["properties"].pop(i, None) - if required := body.get("required"): - body["required"] = list(filter(lambda x: x not in exclude, required)) - - if params_number == 1 and not use_original_model: - param_body: Dict[str, Any] = body.get("properties", {}) - param_body = param_body[name] - - if defs := body.get(DEF_KEY): - # single argument with useless reference - if param_body.get("$ref"): - ref_obj: Dict[str, Any] = next(iter(defs.values())) - ref_obj[DEF_KEY] = { - k: v for k, v in defs.items() if k != ref_obj.get("title") - } - return ref_obj - else: - param_body[DEF_KEY] = defs - - original_title = param.title if PYDANTIC_V2 else param.field_info.title - - if original_title: - use_original_model = True - param_body["title"] = original_title - else: - param_body["title"] = name - - body = param_body - - if not use_original_model: - body["title"] = f"{prefix}:Payload" - - return body diff --git a/faststream/asyncapi/proto.py b/faststream/asyncapi/proto.py deleted file mode 100644 index 81a76da837..0000000000 --- a/faststream/asyncapi/proto.py +++ /dev/null @@ -1,64 +0,0 @@ -from abc import abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Optional, Protocol, Sequence, Union - -if TYPE_CHECKING: - from faststream.asyncapi.schema import ( - Contact, - ContactDict, - ExternalDocs, - ExternalDocsDict, - License, - LicenseDict, - Tag, - TagDict, - ) - from faststream.asyncapi.schema.channels import Channel - from faststream.broker.core.usecase import BrokerUsecase - from faststream.types import ( - AnyDict, - AnyHttpUrl, - ) - - -class AsyncAPIApplication(Protocol): - broker: Optional["BrokerUsecase[Any, Any]"] - - title: str - version: str - description: str - terms_of_service: Optional["AnyHttpUrl"] - license: Optional[Union["License", "LicenseDict", "AnyDict"]] - contact: Optional[Union["Contact", "ContactDict", "AnyDict"]] - asyncapi_tags: Optional[Sequence[Union["Tag", "TagDict", "AnyDict"]]] - external_docs: Optional[Union["ExternalDocs", "ExternalDocsDict", "AnyDict"]] - identifier: Optional[str] - - -class AsyncAPIProto(Protocol): - """A class representing an asynchronous API operation.""" - - title_: Optional[str] - """AsyncAPI object title.""" - - description_: Optional[str] - """AsyncAPI object description.""" - - include_in_schema: bool - """Whetever to include operation in AsyncAPI schema or not.""" - - @property - @abstractmethod - def name(self) -> str: - """Returns the name of the API operation.""" - ... - - @property - @abstractmethod - def description(self) -> Optional[str]: - """Returns the description of the API operation.""" - ... - - @abstractmethod - def schema(self) -> Dict[str, "Channel"]: - """Generate AsyncAPI schema.""" - ... diff --git a/faststream/asyncapi/schema/__init__.py b/faststream/asyncapi/schema/__init__.py deleted file mode 100644 index b9d626e5c6..0000000000 --- a/faststream/asyncapi/schema/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -"""AsyncAPI schema related functions.""" - -from faststream.asyncapi.schema.bindings import ( - ChannelBinding, - OperationBinding, - ServerBinding, -) -from faststream.asyncapi.schema.channels import Channel -from faststream.asyncapi.schema.info import ( - Contact, - ContactDict, - Info, - License, - LicenseDict, -) -from faststream.asyncapi.schema.main import ASYNC_API_VERSION, Components, Schema -from faststream.asyncapi.schema.message import CorrelationId, Message -from faststream.asyncapi.schema.operations import Operation -from faststream.asyncapi.schema.security import SecuritySchemaComponent -from faststream.asyncapi.schema.servers import Server -from faststream.asyncapi.schema.utils import ( - ExternalDocs, - ExternalDocsDict, - Reference, - Tag, - TagDict, -) - -__all__ = ( - # main - "ASYNC_API_VERSION", - # channels - "Channel", - "ChannelBinding", - "Components", - "Contact", - "ContactDict", - "CorrelationId", - "ExternalDocs", - "ExternalDocsDict", - # info - "Info", - "License", - "LicenseDict", - # messages - "Message", - # subscription - "Operation", - "OperationBinding", - "Reference", - "Schema", - # security - "SecuritySchemaComponent", - # servers - "Server", - # bindings - "ServerBinding", - # utils - "Tag", - "TagDict", -) diff --git a/faststream/asyncapi/schema/bindings/__init__.py b/faststream/asyncapi/schema/bindings/__init__.py deleted file mode 100644 index 4b29e49a83..0000000000 --- a/faststream/asyncapi/schema/bindings/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""AsyncAPI schema bindings related functions.""" - -from faststream.asyncapi.schema.bindings.main import ( - ChannelBinding, - OperationBinding, - ServerBinding, -) - -__all__ = ( - "ChannelBinding", - "OperationBinding", - "ServerBinding", -) diff --git a/faststream/asyncapi/schema/bindings/amqp.py b/faststream/asyncapi/schema/bindings/amqp.py deleted file mode 100644 index 8d9ead8dd0..0000000000 --- a/faststream/asyncapi/schema/bindings/amqp.py +++ /dev/null @@ -1,99 +0,0 @@ -"""AsyncAPI AMQP bindings. - -References: https://github.com/asyncapi/bindings/tree/master/amqp -""" - -from typing import Literal, Optional - -from pydantic import BaseModel, Field, PositiveInt - - -class Queue(BaseModel): - """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 = "/" - - -class Exchange(BaseModel): - """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", - "topic", - "fanout", - "headers", - "x-delayed-message", - "x-consistent-hash", - "x-modulus-hash", - ] - - name: Optional[str] = None - durable: Optional[bool] = None - autoDelete: Optional[bool] = None - vhost: str = "/" - - -class ServerBinding(BaseModel): - """A class to represent a server binding. - - Attributes: - bindingVersion : version of the binding (default: "0.2.0") - """ - - bindingVersion: str = "0.2.0" - - -class ChannelBinding(BaseModel): - """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"] = Field(..., alias="is") - bindingVersion: str = "0.2.0" - queue: Optional[Queue] = None - exchange: Optional[Exchange] = None - - -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 - """ - - cc: Optional[str] = None - ack: bool = True - replyTo: Optional[str] = None - deliveryMode: Optional[int] = None - mandatory: Optional[bool] = None - priority: Optional[PositiveInt] = None - bindingVersion: str = "0.2.0" diff --git a/faststream/asyncapi/schema/bindings/kafka.py b/faststream/asyncapi/schema/bindings/kafka.py deleted file mode 100644 index 8f54abb0aa..0000000000 --- a/faststream/asyncapi/schema/bindings/kafka.py +++ /dev/null @@ -1,52 +0,0 @@ -"""AsyncAPI Kafka bindings. - -References: https://github.com/asyncapi/bindings/tree/master/kafka -""" - -from typing import Any, Dict, Optional - -from pydantic import BaseModel, PositiveInt - - -class ServerBinding(BaseModel): - """A class to represent a server binding. - - Attributes: - bindingVersion : version of the binding (default: "0.4.0") - """ - - bindingVersion: str = "0.4.0" - - -class ChannelBinding(BaseModel): - """A class to represent a channel binding. - - Attributes: - 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[PositiveInt] = None - replicas: Optional[PositiveInt] = None - # TODO: - # topicConfiguration - bindingVersion: str = "0.4.0" - - -class OperationBinding(BaseModel): - """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") - """ - - groupId: Optional[Dict[str, Any]] = None - clientId: Optional[Dict[str, Any]] = None - replyTo: Optional[Dict[str, Any]] = None - bindingVersion: str = "0.4.0" diff --git a/faststream/asyncapi/schema/bindings/main.py b/faststream/asyncapi/schema/bindings/main.py deleted file mode 100644 index a20ba0cf3d..0000000000 --- a/faststream/asyncapi/schema/bindings/main.py +++ /dev/null @@ -1,93 +0,0 @@ -from typing import Optional - -from pydantic import BaseModel - -from faststream._compat import PYDANTIC_V2 -from faststream.asyncapi.schema.bindings import amqp as amqp_bindings -from faststream.asyncapi.schema.bindings import http as http_bindings -from faststream.asyncapi.schema.bindings import kafka as kafka_bindings -from faststream.asyncapi.schema.bindings import nats as nats_bindings -from faststream.asyncapi.schema.bindings import redis as redis_bindings -from faststream.asyncapi.schema.bindings import sqs as sqs_bindings - - -class ServerBinding(BaseModel): - """A class to represent server bindings. - - Attributes: - amqp : AMQP server binding (optional) - kafka : Kafka server binding (optional) - sqs : SQS server binding (optional) - nats : NATS server binding (optional) - redis : Redis server binding (optional) - - """ - - amqp: Optional[amqp_bindings.ServerBinding] = None - kafka: Optional[kafka_bindings.ServerBinding] = None - sqs: Optional[sqs_bindings.ServerBinding] = None - nats: Optional[nats_bindings.ServerBinding] = None - redis: Optional[redis_bindings.ServerBinding] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" - - -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" - - -class OperationBinding(BaseModel): - """A class to represent an operation binding. - - Attributes: - amqp : AMQP operation binding (optional) - kafka : Kafka operation binding (optional) - sqs : SQS operation binding (optional) - nats : NATS operation binding (optional) - redis : Redis operation binding (optional) - - """ - - amqp: Optional[amqp_bindings.OperationBinding] = None - kafka: Optional[kafka_bindings.OperationBinding] = None - sqs: Optional[sqs_bindings.OperationBinding] = None - nats: Optional[nats_bindings.OperationBinding] = None - redis: Optional[redis_bindings.OperationBinding] = None - http: Optional[http_bindings.OperationBinding] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" diff --git a/faststream/asyncapi/schema/bindings/nats.py b/faststream/asyncapi/schema/bindings/nats.py deleted file mode 100644 index 3016c91075..0000000000 --- a/faststream/asyncapi/schema/bindings/nats.py +++ /dev/null @@ -1,44 +0,0 @@ -"""AsyncAPI NATS bindings. - -References: https://github.com/asyncapi/bindings/tree/master/nats -""" - -from typing import Any, Dict, Optional - -from pydantic import BaseModel - - -class ServerBinding(BaseModel): - """A class to represent a server binding. - - Attributes: - bindingVersion : version of the binding (default: "custom") - """ - - bindingVersion: str = "custom" - - -class ChannelBinding(BaseModel): - """A class to represent channel binding. - - 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" - - -class OperationBinding(BaseModel): - """A class to represent an operation binding. - - Attributes: - replyTo : optional dictionary containing reply information - bindingVersion : version of the binding (default is "custom") - """ - - replyTo: Optional[Dict[str, Any]] = None - bindingVersion: str = "custom" diff --git a/faststream/asyncapi/schema/bindings/redis.py b/faststream/asyncapi/schema/bindings/redis.py deleted file mode 100644 index fe82e94d1f..0000000000 --- a/faststream/asyncapi/schema/bindings/redis.py +++ /dev/null @@ -1,46 +0,0 @@ -"""AsyncAPI Redis bindings. - -References: https://github.com/asyncapi/bindings/tree/master/redis -""" - -from typing import Any, Dict, Optional - -from pydantic import BaseModel - - -class ServerBinding(BaseModel): - """A class to represent a server binding. - - Attributes: - bindingVersion : version of the binding (default: "custom") - """ - - bindingVersion: str = "custom" - - -class ChannelBinding(BaseModel): - """A class to represent channel binding. - - 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" - - -class OperationBinding(BaseModel): - """A class to represent an operation binding. - - Attributes: - replyTo : optional dictionary containing reply information - bindingVersion : version of the binding (default is "custom") - """ - - replyTo: Optional[Dict[str, Any]] = None - bindingVersion: str = "custom" diff --git a/faststream/asyncapi/schema/bindings/sqs.py b/faststream/asyncapi/schema/bindings/sqs.py deleted file mode 100644 index 0aba239d8c..0000000000 --- a/faststream/asyncapi/schema/bindings/sqs.py +++ /dev/null @@ -1,42 +0,0 @@ -"""AsyncAPI SQS bindings. - -References: https://github.com/asyncapi/bindings/tree/master/sqs -""" - -from typing import Any, Dict, Optional - -from pydantic import BaseModel - - -class ServerBinding(BaseModel): - """A class to represent a server binding. - - Attributes: - bindingVersion : version of the binding (default: "custom") - """ - - bindingVersion: str = "custom" - - -class ChannelBinding(BaseModel): - """A class to represent channel binding. - - Attributes: - queue : a dictionary representing the queue - bindingVersion : a string representing the binding version (default: "custom") - """ - - queue: Dict[str, Any] - bindingVersion: str = "custom" - - -class OperationBinding(BaseModel): - """A class to represent an operation binding. - - Attributes: - replyTo : optional dictionary containing reply information - bindingVersion : version of the binding, default is "custom" - """ - - replyTo: Optional[Dict[str, Any]] = None - bindingVersion: str = "custom" diff --git a/faststream/asyncapi/schema/channels.py b/faststream/asyncapi/schema/channels.py deleted file mode 100644 index cfee0d342b..0000000000 --- a/faststream/asyncapi/schema/channels.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import List, Optional - -from pydantic import BaseModel - -from faststream._compat import PYDANTIC_V2 -from faststream.asyncapi.schema.bindings import ChannelBinding -from faststream.asyncapi.schema.operations import Operation -from faststream.asyncapi.schema.utils import Parameter - - -class Channel(BaseModel): - """A class to represent a channel. - - 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 - parameters : optional parameters associated with the channel - - 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 - servers: Optional[List[str]] = None - bindings: Optional[ChannelBinding] = None - subscribe: Optional[Operation] = None - publish: Optional[Operation] = None - parameters: Optional[Parameter] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" diff --git a/faststream/asyncapi/schema/info.py b/faststream/asyncapi/schema/info.py deleted file mode 100644 index 1e5a1a2d6f..0000000000 --- a/faststream/asyncapi/schema/info.py +++ /dev/null @@ -1,185 +0,0 @@ -from typing import Any, Callable, Dict, Iterable, Optional, Type, Union - -from pydantic import AnyHttpUrl, BaseModel -from typing_extensions import Required, TypedDict - -from faststream._compat import ( - PYDANTIC_V2, - CoreSchema, - GetJsonSchemaHandler, - JsonSchemaValue, - with_info_plain_validator_function, -) -from faststream.log import logger - -try: - import email_validator - - if email_validator is None: - raise ImportError - from pydantic import EmailStr - -except ImportError: # pragma: no cover - # NOTE: EmailStr mock was copied from the FastAPI - # https://github.com/tiangolo/fastapi/blob/master/fastapi/openapi/models.py#24 - class EmailStr(str): # type: ignore - """EmailStr is a string that should be an email. - - Note: EmailStr mock was copied from the FastAPI: - https://github.com/tiangolo/fastapi/blob/master/fastapi/openapi/models.py#24 - - """ - - @classmethod - def __get_validators__(cls) -> Iterable[Callable[..., Any]]: - """Returns the validators for the EmailStr class.""" - yield cls.validate - - @classmethod - def validate(cls, v: Any) -> str: - """Validates the EmailStr class.""" - logger.warning( - "email-validator bot installed, email fields will be treated as str.\n" - "To install, run: pip install email-validator" - ) - return str(v) - - @classmethod - def _validate(cls, __input_value: Any, _: Any) -> str: - logger.warning( - "email-validator bot installed, email fields will be treated as str.\n" - "To install, run: pip install email-validator" - ) - return str(__input_value) - - @classmethod - def __get_pydantic_json_schema__( - cls, - core_schema: CoreSchema, - handler: GetJsonSchemaHandler, - ) -> JsonSchemaValue: - """Returns the JSON schema for the EmailStr class. - - Args: - core_schema : the core schema - handler : the handler - """ - return {"type": "string", "format": "email"} - - @classmethod - def __get_pydantic_core_schema__( - cls, - source: Type[Any], - handler: Callable[[Any], CoreSchema], - ) -> JsonSchemaValue: - """Returns the core schema for the EmailStr class. - - Args: - source : the source - handler : the handler - """ - return with_info_plain_validator_function(cls._validate) - - -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" - - -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" - - -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) - - """ - - title: str - version: str = "1.0.0" - description: str = "" - termsOfService: Optional[AnyHttpUrl] = None - contact: Optional[Union[Contact, ContactDict, Dict[str, Any]]] = None - license: Optional[Union[License, LicenseDict, Dict[str, Any]]] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" diff --git a/faststream/asyncapi/schema/main.py b/faststream/asyncapi/schema/main.py deleted file mode 100644 index 43c47b59c6..0000000000 --- a/faststream/asyncapi/schema/main.py +++ /dev/null @@ -1,125 +0,0 @@ -from typing import Any, Dict, List, Optional, Union - -from pydantic import BaseModel - -from faststream._compat import PYDANTIC_V2, model_to_json, model_to_jsonable -from faststream.asyncapi.schema.channels import Channel -from faststream.asyncapi.schema.info import Info -from faststream.asyncapi.schema.message import Message -from faststream.asyncapi.schema.servers import Server -from faststream.asyncapi.schema.utils import ( - ExternalDocs, - Tag, -) -from faststream.exceptions import INSTALL_YAML - -ASYNC_API_VERSION = "2.6.0" - - -class Components(BaseModel): - # TODO - # servers - # serverVariables - # channels - """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 - - parameters - - correlationIds - - operationTraits - - messageTraits - - serverBindings - - channelBindings - - operationBindings - - messageBindings - - """ - - messages: Optional[Dict[str, Message]] = None - schemas: Optional[Dict[str, Dict[str, Any]]] = None - securitySchemes: Optional[Dict[str, Dict[str, Any]]] = None - # parameters - # correlationIds - # operationTraits - # messageTraits - # serverBindings - # channelBindings - # operationBindings - # messageBindings - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" - - -class Schema(BaseModel): - """A class to represent a schema. - - Attributes: - asyncapi : version of the async API - id : optional ID - defaultContentType : optional default content type - info : information about the schema - servers : optional dictionary of servers - channels : dictionary of channels - components : optional components of the schema - tags : optional list of tags - externalDocs : optional external documentation - - Methods: - to_jsonable() -> Any: Convert the schema to a JSON-serializable object. - to_json() -> str: Convert the schema to a JSON string. - to_yaml() -> str: Convert the schema to a YAML string. - """ - - asyncapi: str = ASYNC_API_VERSION - id: Optional[str] = None - defaultContentType: Optional[str] = None - info: Info - servers: Optional[Dict[str, Server]] = None - channels: Dict[str, Channel] - components: Optional[Components] = None - tags: Optional[List[Union[Tag, Dict[str, Any]]]] = None - externalDocs: Optional[Union[ExternalDocs, Dict[str, Any]]] = None - - def to_jsonable(self) -> Any: - """Convert the schema to a JSON-serializable object.""" - return model_to_jsonable( - self, - by_alias=True, - exclude_none=True, - ) - - def to_json(self) -> str: - """Convert the schema to a JSON string.""" - return model_to_json( - self, - by_alias=True, - exclude_none=True, - ) - - def to_yaml(self) -> str: - """Convert the schema to a YAML string.""" - from io import StringIO - - try: - import yaml - except ImportError as e: - raise ImportError(INSTALL_YAML) from e - - io = StringIO(initial_value="", newline="\n") - yaml.dump(self.to_jsonable(), io, sort_keys=False) - return io.getvalue() diff --git a/faststream/asyncapi/schema/message.py b/faststream/asyncapi/schema/message.py deleted file mode 100644 index 3c9a09f22e..0000000000 --- a/faststream/asyncapi/schema/message.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import Any, Dict, List, Optional, Union - -from pydantic import BaseModel - -from faststream._compat import PYDANTIC_V2 -from faststream.asyncapi.schema.utils import ( - ExternalDocs, - Tag, -) - - -class CorrelationId(BaseModel): - """A class to represent a correlation ID. - - Attributes: - description : optional description of the correlation ID - location : location of the correlation ID - - Configurations: - extra : allows extra fields in the correlation ID model - - """ - - description: Optional[str] = None - location: str - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" - - -class Message(BaseModel): - """A class to represent a message. - - 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 - - """ - - 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 - - payload: Dict[str, Any] - # TODO: - # headers - # schemaFormat - # bindings - # examples - # traits - - tags: Optional[List[Union[Tag, Dict[str, Any]]]] = ( - None # TODO: weird TagDict behavior - ) - externalDocs: Optional[Union[ExternalDocs, Dict[str, Any]]] = ( - None # TODO: weird ExternalDocsDict behavior - ) - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" diff --git a/faststream/asyncapi/schema/operations.py b/faststream/asyncapi/schema/operations.py deleted file mode 100644 index ab73993469..0000000000 --- a/faststream/asyncapi/schema/operations.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import Any, Dict, List, Optional, Union - -from pydantic import BaseModel - -from faststream._compat import PYDANTIC_V2 -from faststream.asyncapi.schema.bindings import OperationBinding -from faststream.asyncapi.schema.message import Message -from faststream.asyncapi.schema.utils import ( - ExternalDocs, - ExternalDocsDict, - Reference, - Tag, - TagDict, -) - - -class Operation(BaseModel): - """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 - externalDocs : external documentation for the operation - - """ - - operationId: Optional[str] = None - summary: Optional[str] = None - description: Optional[str] = None - - bindings: Optional[OperationBinding] = None - - message: Union[Message, Reference, None] = None - - security: Optional[Dict[str, List[str]]] = None - - # TODO - # traits - - tags: Optional[List[Union[Tag, TagDict, Dict[str, Any]]]] = None - externalDocs: Optional[Union[ExternalDocs, ExternalDocsDict, Dict[str, Any]]] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" diff --git a/faststream/asyncapi/schema/security.py b/faststream/asyncapi/schema/security.py deleted file mode 100644 index a157dc5cf5..0000000000 --- a/faststream/asyncapi/schema/security.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import Dict, Literal, Optional - -from pydantic import AnyHttpUrl, BaseModel, Field - -from faststream._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/asyncapi/schema/servers.py b/faststream/asyncapi/schema/servers.py deleted file mode 100644 index 06e2829c69..0000000000 --- a/faststream/asyncapi/schema/servers.py +++ /dev/null @@ -1,74 +0,0 @@ -from typing import Any, Dict, List, Optional, Union - -from pydantic import BaseModel - -from faststream._compat import PYDANTIC_V2 -from faststream.asyncapi.schema.bindings import ServerBinding -from faststream.asyncapi.schema.utils import Reference, Tag, TagDict - -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 - bindings : optional server binding - - 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, TagDict, Dict[str, Any]]]] = None - security: Optional[SecurityRequirement] = None - variables: Optional[Dict[str, Union[ServerVariable, Reference]]] = None - bindings: Optional[Union[ServerBinding, Reference]] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" diff --git a/faststream/asyncapi/schema/utils.py b/faststream/asyncapi/schema/utils.py deleted file mode 100644 index 6857f93552..0000000000 --- a/faststream/asyncapi/schema/utils.py +++ /dev/null @@ -1,96 +0,0 @@ -from typing import Optional, Union - -from pydantic import AnyHttpUrl, BaseModel, Field -from typing_extensions import Required, TypedDict - -from faststream._compat import PYDANTIC_V2 - - -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[AnyHttpUrl] - description: str - - -class ExternalDocs(BaseModel): - """A class to represent external documentation. - - Attributes: - url : URL of the external documentation - description : optional description of the external documentation - - """ - - url: AnyHttpUrl - description: Optional[str] = None - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" - - -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] - - -class Tag(BaseModel): - """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 - - if PYDANTIC_V2: - model_config = {"extra": "allow"} - - else: - - class Config: - extra = "allow" - - -class Reference(BaseModel): - """A class to represent a reference. - - Attributes: - ref : the reference string - - """ - - ref: str = Field(..., alias="$ref") - - -class Parameter(BaseModel): - """A class to represent a parameter.""" - - # TODO - ... diff --git a/faststream/asyncapi/utils.py b/faststream/asyncapi/utils.py deleted file mode 100644 index 4edddae6ad..0000000000 --- a/faststream/asyncapi/utils.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import TYPE_CHECKING, List, Tuple - -if TYPE_CHECKING: - from faststream.types import AnyDict - - -def to_camelcase(*names: str) -> str: - return " ".join(names).replace("_", " ").title().replace(" ", "") - - -def resolve_payloads( - payloads: List[Tuple["AnyDict", str]], - extra: str = "", - served_words: int = 1, -) -> "AnyDict": - ln = len(payloads) - payload: AnyDict - if ln > 1: - one_of_payloads = {} - - for body, handler_name in payloads: - title = body["title"] - words = title.split(":") - - if len(words) > 1: # not pydantic model case - body["title"] = title = ":".join( - filter( - lambda x: bool(x), - ( - handler_name, - extra if extra not in words else "", - *words[served_words:], - ), - ) - ) - - one_of_payloads[title] = body - - payload = {"oneOf": one_of_payloads} - - elif ln == 1: - payload = payloads[0][0] - - else: - payload = {} - - return payload diff --git a/faststream/broker/__init__.py b/faststream/broker/__init__.py deleted file mode 100644 index 65cec1736e..0000000000 --- a/faststream/broker/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Brokers related functions.""" diff --git a/faststream/broker/acknowledgement_watcher.py b/faststream/broker/acknowledgement_watcher.py deleted file mode 100644 index 4084274095..0000000000 --- a/faststream/broker/acknowledgement_watcher.py +++ /dev/null @@ -1,220 +0,0 @@ -import logging -from abc import ABC, abstractmethod -from collections import Counter -from typing import TYPE_CHECKING, Any, Optional, Type, Union -from typing import Counter as CounterType - -from faststream.exceptions import ( - AckMessage, - HandlerException, - NackMessage, - RejectMessage, - SkipMessage, -) - -if TYPE_CHECKING: - from types import TracebackType - - from faststream.broker.message import StreamMessage - from faststream.broker.types import MsgType - from faststream.types import LoggerProto - - -class BaseWatcher(ABC): - """A base class for a watcher.""" - - max_tries: int - - def __init__( - self, - max_tries: int = 0, - logger: Optional["LoggerProto"] = None, - ) -> None: - self.logger = logger - self.max_tries = max_tries - - @abstractmethod - def add(self, message_id: str) -> None: - """Add a message.""" - raise NotImplementedError() - - @abstractmethod - def is_max(self, message_id: str) -> bool: - """Check if the given message ID is the maximum attempt.""" - raise NotImplementedError() - - @abstractmethod - def remove(self, message_id: str) -> None: - """Remove a message.""" - raise NotImplementedError() - - -class EndlessWatcher(BaseWatcher): - """A class to watch and track messages.""" - - def add(self, message_id: str) -> None: - """Add a message to the list.""" - pass - - def is_max(self, message_id: str) -> bool: - """Check if the given message ID is the maximum attempt.""" - return False - - def remove(self, message_id: str) -> None: - """Remove a message.""" - pass - - -class OneTryWatcher(BaseWatcher): - """A class to watch and track messages.""" - - def add(self, message_id: str) -> None: - """Add a message.""" - pass - - def is_max(self, message_id: str) -> bool: - """Check if the given message ID is the maximum attempt.""" - return True - - def remove(self, message_id: str) -> None: - """Remove a message.""" - pass - - -class CounterWatcher(BaseWatcher): - """A class to watch and track the count of messages.""" - - memory: CounterType[str] - - def __init__( - self, - max_tries: int = 3, - logger: Optional["LoggerProto"] = None, - ) -> None: - super().__init__(logger=logger, max_tries=max_tries) - self.memory = Counter() - - def add(self, message_id: str) -> None: - """Check if the given message ID is the maximum attempt.""" - self.memory[message_id] += 1 - - def is_max(self, message_id: str) -> bool: - """Check if the number of tries for a message has exceeded the maximum allowed tries.""" - is_max = self.memory[message_id] > self.max_tries - if self.logger is not None: - if is_max: - self.logger.log( - logging.ERROR, f"Already retried {self.max_tries} times. Skipped." - ) - else: - self.logger.log( - logging.ERROR, "Error is occurred. Pushing back to queue." - ) - return is_max - - def remove(self, message_id: str) -> None: - """Remove a message from memory.""" - self.memory[message_id] = 0 - self.memory += Counter() - - -class WatcherContext: - """A class representing a context for a watcher.""" - - def __init__( - self, - message: "StreamMessage[MsgType]", - watcher: BaseWatcher, - logger: Optional["LoggerProto"] = None, - **extra_options: Any, - ) -> None: - self.watcher = watcher - self.message = message - self.extra_options = extra_options - self.logger = logger - - async def __aenter__(self) -> None: - self.watcher.add(self.message.message_id) - - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional["TracebackType"], - ) -> bool: - """Exit the asynchronous context manager.""" - if not exc_type: - await self.__ack() - - elif isinstance(exc_val, HandlerException): - if isinstance(exc_val, SkipMessage): - self.watcher.remove(self.message.message_id) - - elif isinstance(exc_val, AckMessage): - await self.__ack(**exc_val.extra_options) - - elif isinstance(exc_val, NackMessage): - await self.__nack(**exc_val.extra_options) - - elif isinstance(exc_val, RejectMessage): # pragma: no branch - await self.__reject(**exc_val.extra_options) - - # Exception was processed and suppressed - return True - - elif self.watcher.is_max(self.message.message_id): - await self.__reject() - - else: - await self.__nack() - - # Exception was not processed - return False - - async def __ack(self, **exc_extra_options: Any) -> None: - try: - await self.message.ack(**self.extra_options, **exc_extra_options) - except Exception as er: - if self.logger is not None: - self.logger.log(logging.ERROR, er, exc_info=er) - else: - self.watcher.remove(self.message.message_id) - - async def __nack(self, **exc_extra_options: Any) -> None: - try: - await self.message.nack(**self.extra_options, **exc_extra_options) - except Exception as er: - if self.logger is not None: - self.logger.log(logging.ERROR, er, exc_info=er) - - async def __reject(self, **exc_extra_options: Any) -> None: - try: - await self.message.reject(**self.extra_options, **exc_extra_options) - except Exception as er: - if self.logger is not None: - self.logger.log(logging.ERROR, er, exc_info=er) - else: - self.watcher.remove(self.message.message_id) - - -def get_watcher( - logger: Optional["LoggerProto"], - try_number: Union[bool, int], -) -> BaseWatcher: - """Get a watcher object based on the provided parameters. - - Args: - logger: Optional logger object for logging messages. - try_number: Optional parameter to specify the type of watcher. - - If set to True, an EndlessWatcher object will be returned. - - If set to False, a OneTryWatcher object will be returned. - - If set to an integer, a CounterWatcher object with the specified maximum number of tries will be returned. - """ - watcher: Optional[BaseWatcher] - if try_number is True: - watcher = EndlessWatcher() - elif try_number is False: - watcher = OneTryWatcher() - else: - watcher = CounterWatcher(logger=logger, max_tries=try_number) - return watcher diff --git a/faststream/broker/core/abc.py b/faststream/broker/core/abc.py deleted file mode 100644 index c514814b96..0000000000 --- a/faststream/broker/core/abc.py +++ /dev/null @@ -1,148 +0,0 @@ -from abc import abstractmethod -from typing import ( - TYPE_CHECKING, - Any, - Generic, - Iterable, - Mapping, - Optional, - Sequence, -) - -from faststream.broker.types import MsgType - -if TYPE_CHECKING: - from fast_depends.dependencies import Depends - - from faststream.broker.publisher.proto import PublisherProto - from faststream.broker.subscriber.proto import SubscriberProto - from faststream.broker.types import ( - BrokerMiddleware, - CustomCallable, - ) - - -class ABCBroker(Generic[MsgType]): - _subscribers: Mapping[int, "SubscriberProto[MsgType]"] - _publishers: Mapping[int, "PublisherProto[MsgType]"] - - def __init__( - self, - *, - prefix: str, - dependencies: Iterable["Depends"], - middlewares: Sequence["BrokerMiddleware[MsgType]"], - parser: Optional["CustomCallable"], - decoder: Optional["CustomCallable"], - include_in_schema: Optional[bool], - ) -> None: - self.prefix = prefix - self.include_in_schema = include_in_schema - - self._subscribers = {} - self._publishers = {} - - self._dependencies = dependencies - self._middlewares = middlewares - self._parser = parser - self._decoder = decoder - - def add_middleware(self, middleware: "BrokerMiddleware[MsgType]") -> None: - """Append BrokerMiddleware to the end of middlewares list. - - Current middleware will be used as a most inner of already existed ones. - """ - self._middlewares = (*self._middlewares, middleware) - - for sub in self._subscribers.values(): - sub.add_middleware(middleware) - - for pub in self._publishers.values(): - pub.add_middleware(middleware) - - @abstractmethod - def subscriber( - self, - subscriber: "SubscriberProto[MsgType]", - ) -> "SubscriberProto[MsgType]": - subscriber.add_prefix(self.prefix) - key = hash(subscriber) - subscriber = self._subscribers.get(key, subscriber) - self._subscribers = {**self._subscribers, key: subscriber} - return subscriber - - @abstractmethod - def publisher( - self, - publisher: "PublisherProto[MsgType]", - ) -> "PublisherProto[MsgType]": - publisher.add_prefix(self.prefix) - key = hash(publisher) - publisher = self._publishers.get(key, publisher) - self._publishers = {**self._publishers, key: publisher} - return publisher - - def include_router( - self, - router: "ABCBroker[Any]", - *, - prefix: str = "", - dependencies: Iterable["Depends"] = (), - middlewares: Iterable["BrokerMiddleware[MsgType]"] = (), - include_in_schema: Optional[bool] = None, - ) -> None: - """Includes a router in the current object.""" - for h in router._subscribers.values(): - h.add_prefix("".join((self.prefix, prefix))) - - if (key := hash(h)) not in self._subscribers: - if include_in_schema is None: - h.include_in_schema = self._solve_include_in_schema( - h.include_in_schema - ) - else: - h.include_in_schema = include_in_schema - - h._broker_middlewares = ( - *self._middlewares, - *middlewares, - *h._broker_middlewares, - ) - h._broker_dependencies = ( - *self._dependencies, - *dependencies, - *h._broker_dependencies, - ) - self._subscribers = {**self._subscribers, key: h} - - for p in router._publishers.values(): - p.add_prefix(self.prefix) - - if (key := hash(p)) not in self._publishers: - if include_in_schema is None: - p.include_in_schema = self._solve_include_in_schema( - p.include_in_schema - ) - else: - p.include_in_schema = include_in_schema - - p._broker_middlewares = ( - *self._middlewares, - *middlewares, - *p._broker_middlewares, - ) - self._publishers = {**self._publishers, key: p} - - def include_routers( - self, - *routers: "ABCBroker[MsgType]", - ) -> None: - """Includes routers in the object.""" - for r in routers: - self.include_router(r) - - def _solve_include_in_schema(self, include_in_schema: bool) -> bool: - if self.include_in_schema is None or self.include_in_schema: - return include_in_schema - else: - return self.include_in_schema diff --git a/faststream/broker/core/logging.py b/faststream/broker/core/logging.py deleted file mode 100644 index e6cd8a0f59..0000000000 --- a/faststream/broker/core/logging.py +++ /dev/null @@ -1,108 +0,0 @@ -import logging -import warnings -from abc import abstractmethod -from typing import TYPE_CHECKING, Any, Optional - -from typing_extensions import Annotated, Doc, deprecated - -from faststream.broker.core.abc import ABCBroker -from faststream.broker.types import MsgType -from faststream.types import EMPTY - -if TYPE_CHECKING: - from faststream.types import AnyDict, LoggerProto - - -class LoggingBroker(ABCBroker[MsgType]): - """A mixin class for logging.""" - - logger: Optional["LoggerProto"] - - @abstractmethod - def get_fmt(self) -> str: - """Fallback method to get log format if `log_fmt` if not specified.""" - raise NotImplementedError() - - @abstractmethod - def _setup_log_context(self) -> None: - raise NotImplementedError() - - def __init__( - self, - *args: Any, - default_logger: Annotated[ - logging.Logger, - Doc("Logger object to use if `logger` is not set."), - ], - logger: Annotated[ - Optional["LoggerProto"], - Doc("User specified logger to pass into Context and log service messages."), - ], - log_level: Annotated[ - int, - Doc("Service messages log level."), - ], - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - Doc("Default logger log format."), - ] = EMPTY, - **kwargs: Any, - ) -> None: - if logger is not EMPTY: - self.logger = logger - self.use_custom = True - else: - self.logger = default_logger - self.use_custom = False - - self._msg_log_level = log_level - - if log_fmt is not EMPTY: - warnings.warn( - DeprecationWarning( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - stacklevel=2, - ) - self._fmt = log_fmt - else: - self._fmt = None - - super().__init__(*args, **kwargs) - - def _get_fmt(self) -> str: - """Get default logger format at broker startup.""" - return self._fmt or self.get_fmt() - - def _log( - self, - message: Annotated[ - str, - Doc("Log message."), - ], - log_level: Annotated[ - Optional[int], - Doc("Log record level. Use `__init__: log_level` option if not specified."), - ] = None, - extra: Annotated[ - Optional["AnyDict"], - Doc("Log record extra information."), - ] = None, - exc_info: Annotated[ - Optional[Exception], - Doc("Exception object to log traceback."), - ] = None, - ) -> None: - """Logs a message.""" - if self.logger is not None: - self.logger.log( - (log_level or self._msg_log_level), - message, - extra=extra, - exc_info=exc_info, - ) diff --git a/faststream/broker/core/usecase.py b/faststream/broker/core/usecase.py deleted file mode 100644 index d1b7cb792e..0000000000 --- a/faststream/broker/core/usecase.py +++ /dev/null @@ -1,391 +0,0 @@ -import logging -from abc import abstractmethod -from contextlib import AsyncExitStack -from functools import partial -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Generic, - Iterable, - List, - Optional, - Sequence, - Type, - Union, - cast, -) - -from typing_extensions import Annotated, Doc, Self, deprecated - -from faststream._compat import is_test_env -from faststream.broker.core.logging import LoggingBroker -from faststream.broker.message import SourceType -from faststream.broker.middlewares.logging import CriticalLogMiddleware -from faststream.broker.proto import SetupAble -from faststream.broker.subscriber.proto import SubscriberProto -from faststream.broker.types import ( - AsyncCustomCallable, - BrokerMiddleware, - ConnectionType, - CustomCallable, - MsgType, -) -from faststream.exceptions import NOT_CONNECTED_YET -from faststream.log.logging import set_logger_fmt -from faststream.types import EMPTY -from faststream.utils.context.repository import context -from faststream.utils.functions import return_input, to_async - -if TYPE_CHECKING: - from types import TracebackType - - from fast_depends.dependencies import Depends - - from faststream.asyncapi.schema import Tag, TagDict - from faststream.broker.message import StreamMessage - from faststream.broker.publisher.proto import ProducerProto, PublisherProto - from faststream.security import BaseSecurity - from faststream.types import AnyDict, Decorator, LoggerProto - - -class BrokerUsecase( - LoggingBroker[MsgType], - SetupAble, - Generic[MsgType, ConnectionType], -): - """A class representing a broker async use case.""" - - url: Union[str, Sequence[str]] - _connection: Optional[ConnectionType] - _producer: Optional["ProducerProto"] - - def __init__( - self, - *, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Custom decoder object."), - ], - parser: Annotated[ - Optional["CustomCallable"], - Doc("Custom parser object."), - ], - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies to apply to all broker subscribers."), - ], - middlewares: Annotated[ - Sequence["BrokerMiddleware[MsgType]"], - Doc("Middlewares to apply to all broker publishers/subscribers."), - ], - graceful_timeout: Annotated[ - Optional[float], - Doc( - "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down." - ), - ], - # Logging args - default_logger: Annotated[ - logging.Logger, - Doc("Logger object to use if `logger` is not set."), - ], - logger: Annotated[ - Optional["LoggerProto"], - Doc("User specified logger to pass into Context and log service messages."), - ], - log_level: Annotated[ - int, - Doc("Service messages log level."), - ], - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - Doc("Default logger log format."), - ] = EMPTY, - # FastDepends args - apply_types: Annotated[ - bool, - Doc("Whether to use FastDepends or not."), - ], - validate: Annotated[ - bool, - Doc("Whether to cast types using Pydantic validation."), - ], - _get_dependant: Annotated[ - Optional[Callable[..., Any]], - Doc("Custom library dependant generator callback."), - ], - _call_decorators: Annotated[ - Iterable["Decorator"], - Doc("Any custom decorator to apply to wrapped functions."), - ], - # AsyncAPI kwargs - protocol: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol."), - ], - protocol_version: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol version."), - ], - description: Annotated[ - Optional[str], - Doc("AsyncAPI server description."), - ], - tags: Annotated[ - Optional[Iterable[Union["Tag", "TagDict"]]], - Doc("AsyncAPI server tags."), - ], - asyncapi_url: Annotated[ - Union[str, List[str]], - Doc("AsyncAPI hardcoded server addresses."), - ], - security: Annotated[ - Optional["BaseSecurity"], - Doc( - "Security options to connect broker and generate AsyncAPI server security." - ), - ], - **connection_kwargs: Any, - ) -> None: - super().__init__( - middlewares=middlewares, - dependencies=dependencies, - decoder=cast( - "Optional[AsyncCustomCallable]", - to_async(decoder) if decoder else None, - ), - parser=cast( - "Optional[AsyncCustomCallable]", - to_async(parser) if parser else None, - ), - # Broker is a root router - include_in_schema=True, - prefix="", - # Logging args - default_logger=default_logger, - log_level=log_level, - log_fmt=log_fmt, - logger=logger, - ) - - self.running = False - self.graceful_timeout = graceful_timeout - - self._connection_kwargs = connection_kwargs - self._connection = None - self._producer = None - - # TODO: remove useless middleware filter - if not is_test_env(): - self._middlewares = ( - CriticalLogMiddleware(self.logger, log_level), - *self._middlewares, - ) - - # TODO: move this context to Handlers' extra_context to support multiple brokers - context.set_global("logger", self.logger) - context.set_global("broker", self) - - # FastDepends args - self._is_apply_types = apply_types - self._is_validate = validate - self._get_dependant = _get_dependant - self._call_decorators = _call_decorators - - # AsyncAPI information - self.url = asyncapi_url - self.protocol = protocol - self.protocol_version = protocol_version - self.description = description - self.tags = tags - self.security = security - - async def __aenter__(self) -> "Self": - await self.connect() - return self - - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional["TracebackType"], - ) -> None: - await self.close(exc_type, exc_val, exc_tb) - - @abstractmethod - async def start(self) -> None: - """Start the broker async use case.""" - self._abc_start() - await self.connect() - - async def connect(self, **kwargs: Any) -> ConnectionType: - """Connect to a remote server.""" - if self._connection is None: - connection_kwargs = self._connection_kwargs.copy() - connection_kwargs.update(kwargs) - self._connection = await self._connect(**connection_kwargs) - self.setup() - return self._connection - - @abstractmethod - async def _connect(self) -> ConnectionType: - """Connect to a resource.""" - raise NotImplementedError() - - def setup(self) -> None: - """Prepare all Broker entities to startup.""" - for h in self._subscribers.values(): - self.setup_subscriber(h) - - for p in self._publishers.values(): - self.setup_publisher(p) - - def setup_subscriber( - self, - subscriber: SubscriberProto[MsgType], - **kwargs: Any, - ) -> None: - """Setup the Subscriber to prepare it to starting.""" - data = self._subscriber_setup_extra.copy() - data.update(kwargs) - subscriber.setup(**data) - - def setup_publisher( - self, - publisher: "PublisherProto[MsgType]", - **kwargs: Any, - ) -> None: - """Setup the Publisher to prepare it to starting.""" - data = self._publisher_setup_extra.copy() - data.update(kwargs) - publisher.setup(**data) - - @property - def _subscriber_setup_extra(self) -> "AnyDict": - return { - "logger": self.logger, - "producer": self._producer, - "graceful_timeout": self.graceful_timeout, - "extra_context": {}, - # broker options - "broker_parser": self._parser, - "broker_decoder": self._decoder, - # dependant args - "apply_types": self._is_apply_types, - "is_validate": self._is_validate, - "_get_dependant": self._get_dependant, - "_call_decorators": self._call_decorators, - } - - @property - def _publisher_setup_extra(self) -> "AnyDict": - return { - "producer": self._producer, - } - - def publisher(self, *args: Any, **kwargs: Any) -> "PublisherProto[MsgType]": - pub = super().publisher(*args, **kwargs) - if self.running or self._connection is not None: - self.setup_publisher(pub) - return pub - - def _abc_start(self) -> None: - for h in self._subscribers.values(): - log_context = h.get_log_context(None) - log_context.pop("message_id", None) - self._setup_log_context(**log_context) - - if not self.running: - self.running = True - - if not self.use_custom and self.logger is not None: - set_logger_fmt( - cast("logging.Logger", self.logger), - self._get_fmt(), - ) - - async def close( - self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> None: - """Closes the object.""" - self.running = False - - for h in self._subscribers.values(): - await h.close() - - if self._connection is not None: - await self._close(exc_type, exc_val, exc_tb) - - @abstractmethod - async def _close( - self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> None: - """Close the object.""" - self._connection = None - - async def publish( - self, - msg: Any, - *, - producer: Optional["ProducerProto"], - correlation_id: Optional[str] = None, - **kwargs: Any, - ) -> Optional[Any]: - """Publish message directly.""" - assert producer, NOT_CONNECTED_YET # nosec B101 - - publish = producer.publish - - for m in self._middlewares[::-1]: - publish = partial(m(None).publish_scope, publish) - - return await publish(msg, correlation_id=correlation_id, **kwargs) - - async def request( - self, - msg: Any, - *, - producer: Optional["ProducerProto"], - correlation_id: Optional[str] = None, - **kwargs: Any, - ) -> Any: - """Publish message directly.""" - assert producer, NOT_CONNECTED_YET # nosec B101 - - request = producer.request - for m in self._middlewares[::-1]: - request = partial(m(None).publish_scope, request) - - published_msg = await request( - msg, - correlation_id=correlation_id, - **kwargs, - ) - - async with AsyncExitStack() as stack: - return_msg = return_input - for m in self._middlewares[::-1]: - mid = m(published_msg) - await stack.enter_async_context(mid) - return_msg = partial(mid.consume_scope, return_msg) - - parsed_msg: StreamMessage[Any] = await producer._parser(published_msg) - parsed_msg._decoded_body = await producer._decoder(parsed_msg) - parsed_msg._source_type = SourceType.Response - return await return_msg(parsed_msg) - - @abstractmethod - async def ping(self, timeout: Optional[float]) -> bool: - """Check connection alive.""" - raise NotImplementedError() diff --git a/faststream/broker/fastapi/__init__.py b/faststream/broker/fastapi/__init__.py deleted file mode 100644 index 4b683d238c..0000000000 --- a/faststream/broker/fastapi/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from faststream.broker.fastapi.route import StreamMessage -from faststream.broker.fastapi.router import StreamRouter - -__all__ = ( - "StreamMessage", - "StreamRouter", -) diff --git a/faststream/broker/fastapi/_compat.py b/faststream/broker/fastapi/_compat.py deleted file mode 100644 index c51826690c..0000000000 --- a/faststream/broker/fastapi/_compat.py +++ /dev/null @@ -1,137 +0,0 @@ -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, List, Optional - -from fastapi import __version__ as FASTAPI_VERSION # noqa: N812 -from fastapi.dependencies.utils import solve_dependencies -from starlette.background import BackgroundTasks -from typing_extensions import Never - -from faststream.types import AnyDict - -if TYPE_CHECKING: - from fastapi.dependencies.models import Dependant - from fastapi.requests import Request - -major, minor, patch, *_ = FASTAPI_VERSION.split(".") - -_FASTAPI_MAJOR, _FASTAPI_MINOR = int(major), int(minor) - -FASTAPI_V2 = _FASTAPI_MAJOR > 0 or _FASTAPI_MINOR > 100 -FASTAPI_V106 = _FASTAPI_MAJOR > 0 or _FASTAPI_MINOR >= 106 - -try: - _FASTAPI_PATCH = int(patch) -except ValueError: - FASTAPI_v102_3 = True - FASTAPI_v102_4 = True -else: - FASTAPI_v102_3 = ( - _FASTAPI_MAJOR > 0 - or _FASTAPI_MINOR > 112 - or (_FASTAPI_MINOR == 112 and _FASTAPI_PATCH > 2) - ) - FASTAPI_v102_4 = ( - _FASTAPI_MAJOR > 0 - or _FASTAPI_MINOR > 112 - or (_FASTAPI_MINOR == 112 and _FASTAPI_PATCH > 3) - ) - -__all__ = ( - "RequestValidationError", - "create_response_field", - "raise_fastapi_validation_error", - "solve_faststream_dependency", -) - - -@dataclass -class SolvedDependency: - values: AnyDict - errors: List[Any] - background_tasks: Optional[BackgroundTasks] - - -if FASTAPI_V2: - from fastapi._compat import _normalize_errors - from fastapi.exceptions import RequestValidationError - - def raise_fastapi_validation_error(errors: List[Any], body: AnyDict) -> Never: - raise RequestValidationError(_normalize_errors(errors), body=body) - -else: - from pydantic import ( # type: ignore[assignment] - ValidationError as RequestValidationError, - ) - from pydantic import create_model - - ROUTER_VALIDATION_ERROR_MODEL = create_model("StreamRoute") - - def raise_fastapi_validation_error(errors: List[Any], body: AnyDict) -> Never: - raise RequestValidationError(errors, ROUTER_VALIDATION_ERROR_MODEL) # type: ignore[misc] - - -if FASTAPI_v102_3: - from fastapi.utils import ( - create_model_field as create_response_field, - ) - - extra = {"embed_body_fields": False} if FASTAPI_v102_4 else {} - - async def solve_faststream_dependency( - request: "Request", - dependant: "Dependant", - dependency_overrides_provider: Optional[Any], - **kwargs: Any, - ) -> SolvedDependency: - solved_result = await solve_dependencies( - request=request, - body=request._body, # type: ignore[arg-type] - dependant=dependant, - dependency_overrides_provider=dependency_overrides_provider, - **extra, # type: ignore[arg-type] - **kwargs, - ) - values, errors, background = ( - solved_result.values, - solved_result.errors, - solved_result.background_tasks, - ) - - return SolvedDependency( - values=values, - errors=errors, - background_tasks=background, - ) - -else: - from fastapi.utils import ( # type: ignore[attr-defined,no-redef] - create_response_field as create_response_field, - ) - - async def solve_faststream_dependency( - request: "Request", - dependant: "Dependant", - dependency_overrides_provider: Optional[Any], - **kwargs: Any, - ) -> SolvedDependency: - solved_result = await solve_dependencies( - request=request, - body=request._body, # type: ignore[arg-type] - dependant=dependant, - dependency_overrides_provider=dependency_overrides_provider, - **kwargs, - ) - - ( - values, - errors, - background, - _response, - _dependency_cache, - ) = solved_result # type: ignore[misc] - - return SolvedDependency( - values=values, # type: ignore[has-type] - errors=errors, # type: ignore[has-type] - background_tasks=background, # type: ignore[has-type] - ) diff --git a/faststream/broker/fastapi/config.py b/faststream/broker/fastapi/config.py deleted file mode 100644 index 9b9c32d645..0000000000 --- a/faststream/broker/fastapi/config.py +++ /dev/null @@ -1,15 +0,0 @@ -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Optional - -if TYPE_CHECKING: - from fastapi import FastAPI - - -@dataclass -class FastAPIConfig: - dependency_overrides_provider: Optional[Any] - application: Optional["FastAPI"] = None - - def set_application(self, app: "FastAPI") -> None: - self.application = app - self.dependency_overrides_provider = self.dependency_overrides_provider or app diff --git a/faststream/broker/fastapi/context.py b/faststream/broker/fastapi/context.py deleted file mode 100644 index 25edd313b7..0000000000 --- a/faststream/broker/fastapi/context.py +++ /dev/null @@ -1,30 +0,0 @@ -import logging -from typing import Any, Callable, Optional - -from fastapi import params -from typing_extensions import Annotated - -from faststream.types import EMPTY -from faststream.utils.context import ContextRepo as CR -from faststream.utils.context.types import resolve_context_by_name - - -def Context( # noqa: N802 - name: str, - *, - default: Any = EMPTY, - initial: Optional[Callable[..., Any]] = None, -) -> Any: - """Get access to objects of the Context.""" - return params.Depends( - lambda: resolve_context_by_name( - name=name, - default=default, - initial=initial, - ), - use_cache=True, - ) - - -Logger = Annotated[logging.Logger, Context("logger")] -ContextRepo = Annotated[CR, Context("context")] diff --git a/faststream/broker/fastapi/router.py b/faststream/broker/fastapi/router.py deleted file mode 100644 index f750ccb9a9..0000000000 --- a/faststream/broker/fastapi/router.py +++ /dev/null @@ -1,535 +0,0 @@ -import json -import warnings -from abc import abstractmethod -from contextlib import asynccontextmanager -from enum import Enum -from typing import ( - TYPE_CHECKING, - Any, - AsyncIterator, - Awaitable, - Callable, - Dict, - Generic, - Iterable, - List, - Mapping, - Optional, - Sequence, - Type, - Union, - cast, - overload, -) - -from fastapi.datastructures import Default -from fastapi.responses import HTMLResponse -from fastapi.routing import APIRoute, APIRouter -from fastapi.utils import generate_unique_id -from starlette.responses import JSONResponse, Response -from starlette.routing import BaseRoute, _DefaultLifespan - -from faststream.asyncapi.proto import AsyncAPIApplication -from faststream.asyncapi.site import get_asyncapi_html -from faststream.broker.middlewares import BaseMiddleware -from faststream.broker.router import BrokerRouter -from faststream.broker.types import ( - MsgType, - P_HandlerParams, - T_HandlerReturn, -) -from faststream.utils.context.repository import context -from faststream.utils.functions import fake_context, to_async - -from .config import FastAPIConfig -from .get_dependant import get_fastapi_dependant -from .route import wrap_callable_to_fastapi_compatible - -if TYPE_CHECKING: - from types import TracebackType - - from fastapi import FastAPI, params - from fastapi.background import BackgroundTasks - from fastapi.types import IncEx - from starlette import routing - from starlette.types import ASGIApp, AppType, Lifespan - - from faststream.asyncapi import schema as asyncapi - from faststream.asyncapi.schema import Schema - from faststream.broker.core.usecase import BrokerUsecase - from faststream.broker.message import StreamMessage - from faststream.broker.publisher.proto import PublisherProto - from faststream.broker.schemas import NameRequired - from faststream.broker.types import BrokerMiddleware - from faststream.broker.wrapper.call import HandlerCallWrapper - from faststream.types import AnyDict - - -class _BackgroundMiddleware(BaseMiddleware): - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> Optional[bool]: - if not exc_type and ( - background := cast( - "Optional[BackgroundTasks]", - getattr(context.get_local("message"), "background", None), - ) - ): - await background() - - return await super().after_processed(exc_type, exc_val, exc_tb) - - -class StreamRouter( - APIRouter, - AsyncAPIApplication, - Generic[MsgType], -): - """A class to route streams.""" - - broker_class: Type["BrokerUsecase[MsgType, Any]"] - broker: "BrokerUsecase[MsgType, Any]" - docs_router: Optional[APIRouter] - _after_startup_hooks: List[Callable[[Any], Awaitable[Optional[Mapping[str, Any]]]]] - _on_shutdown_hooks: List[Callable[[Any], Awaitable[None]]] - schema: Optional["Schema"] - - title: str - description: str - version: str - license: Optional["AnyDict"] - contact: Optional["AnyDict"] - - def __init__( - self, - *connection_args: Any, - middlewares: Sequence["BrokerMiddleware[MsgType]"] = (), - prefix: str = "", - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence["params.Depends"]] = None, - default_response_class: Type["Response"] = Default(JSONResponse), - responses: Optional[Dict[Union[int, str], "AnyDict"]] = None, - callbacks: Optional[List["routing.BaseRoute"]] = None, - routes: Optional[List["routing.BaseRoute"]] = None, - redirect_slashes: bool = True, - default: Optional["ASGIApp"] = None, - dependency_overrides_provider: Optional[Any] = None, - route_class: Type["APIRoute"] = APIRoute, - on_startup: Optional[Sequence[Callable[[], Any]]] = None, - on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - setup_state: bool = True, - lifespan: Optional["Lifespan[Any]"] = None, - generate_unique_id_function: Callable[["APIRoute"], str] = Default( - generate_unique_id - ), - # AsyncAPI information - asyncapi_tags: Optional[ - Iterable[Union["asyncapi.Tag", "asyncapi.TagDict"]] - ] = None, - schema_url: Optional[str] = "/asyncapi", - **connection_kwars: Any, - ) -> None: - assert ( # nosec B101 - self.broker_class - ), "You should specify `broker_class` at your implementation" - - self.broker = self.broker_class( - *connection_args, - middlewares=( - *middlewares, - # allow to catch background exceptions in user middlewares - _BackgroundMiddleware, - ), - _get_dependant=get_fastapi_dependant, - tags=asyncapi_tags, - apply_types=False, - **connection_kwars, - ) - - self.setup_state = setup_state - - # AsyncAPI information - # Empty - self.terms_of_service = None - self.identifier = None - self.asyncapi_tags = None - self.external_docs = None - # parse from FastAPI app on startup - self.title = "" - self.version = "" - self.description = "" - self.license = None - self.contact = None - - self.schema = None - # Flag to prevent double lifespan start - self._lifespan_started = False - - super().__init__( - prefix=prefix, - tags=tags, - dependencies=dependencies, - default_response_class=default_response_class, - responses=responses, - callbacks=callbacks, - routes=routes, - redirect_slashes=redirect_slashes, - default=default, - dependency_overrides_provider=dependency_overrides_provider, - route_class=route_class, - deprecated=deprecated, - include_in_schema=include_in_schema, - generate_unique_id_function=generate_unique_id_function, - lifespan=self._wrap_lifespan(lifespan), - on_startup=on_startup, - on_shutdown=on_shutdown, - ) - - self.fastapi_config = FastAPIConfig( - dependency_overrides_provider=dependency_overrides_provider - ) - - if self.include_in_schema: - self.docs_router = self._asyncapi_router(schema_url) - else: - self.docs_router = None - - self._after_startup_hooks = [] - self._on_shutdown_hooks = [] - - def _add_api_mq_route( - self, - dependencies: Iterable["params.Depends"], - response_model: Any, - response_model_include: Optional["IncEx"], - response_model_exclude: Optional["IncEx"], - response_model_by_alias: bool, - response_model_exclude_unset: bool, - response_model_exclude_defaults: bool, - response_model_exclude_none: bool, - ) -> Callable[ - [Callable[..., Any]], - Callable[["StreamMessage[Any]"], Awaitable[Any]], - ]: - """Decorator before `broker.subscriber`, that wraps function to FastAPI-compatible one.""" - - def wrapper( - endpoint: Callable[..., Any], - ) -> Callable[["StreamMessage[Any]"], Awaitable[Any]]: - """Patch user function to make it FastAPI-compatible.""" - return wrap_callable_to_fastapi_compatible( - user_callable=endpoint, - dependencies=dependencies, - response_model=response_model, - response_model_include=response_model_include, - response_model_exclude=response_model_exclude, - response_model_by_alias=response_model_by_alias, - response_model_exclude_unset=response_model_exclude_unset, - response_model_exclude_defaults=response_model_exclude_defaults, - response_model_exclude_none=response_model_exclude_none, - fastapi_config=self.fastapi_config, - ) - - return wrapper - - def subscriber( - self, - *extra: Union["NameRequired", str], - dependencies: Iterable["params.Depends"], - response_model: Any, - response_model_include: Optional["IncEx"], - response_model_exclude: Optional["IncEx"], - response_model_by_alias: bool, - response_model_exclude_unset: bool, - response_model_exclude_defaults: bool, - response_model_exclude_none: bool, - **broker_kwargs: Any, - ) -> Callable[ - [Callable[P_HandlerParams, T_HandlerReturn]], - "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", - ]: - """A function decorator for subscribing to a message queue.""" - dependencies = (*self.dependencies, *dependencies) - - sub = self.broker.subscriber( # type: ignore[call-arg] - *extra, # type: ignore[arg-type] - dependencies=dependencies, - **broker_kwargs, - ) - - sub._call_decorators = ( # type: ignore[attr-defined] - self._add_api_mq_route( - dependencies=dependencies, - response_model=response_model, - response_model_include=response_model_include, - response_model_exclude=response_model_exclude, - response_model_by_alias=response_model_by_alias, - response_model_exclude_unset=response_model_exclude_unset, - response_model_exclude_defaults=response_model_exclude_defaults, - response_model_exclude_none=response_model_exclude_none, - ), - ) - - return sub - - def _wrap_lifespan( - self, lifespan: Optional["Lifespan[Any]"] = None - ) -> "Lifespan[Any]": - lifespan_context = lifespan if lifespan is not None else _DefaultLifespan(self) - - @asynccontextmanager - async def start_broker_lifespan( - app: "FastAPI", - ) -> AsyncIterator[Mapping[str, Any]]: - """Starts the lifespan of a broker.""" - self.fastapi_config.set_application(app) - - if self.docs_router: - self.title = app.title - self.description = app.description - self.version = app.version - self.contact = app.contact - self.license = app.license_info - - from faststream.asyncapi.generate import get_app_schema - - self.schema = get_app_schema(self) - - app.include_router(self.docs_router) - - async with lifespan_context(app) as maybe_context: - if maybe_context is None: - context: AnyDict = {} - else: - context = dict(maybe_context) - - context.update({"broker": self.broker}) - - if not self._lifespan_started: - await self.broker.start() - self._lifespan_started = True - else: - warnings.warn( - "Specifying 'lifespan_context' manually is no longer necessary with FastAPI >= 0.112.2.", - stacklevel=2, - ) - - for h in self._after_startup_hooks: - h_context = await h(app) - if h_context: # pragma: no branch - context.update(h_context) - - try: - if self.setup_state: - yield context - else: - # NOTE: old asgi compatibility - yield # type: ignore - - for h in self._on_shutdown_hooks: - await h(app) - - finally: - await self.broker.close() - - return start_broker_lifespan - - @overload - def after_startup( - self, - func: Callable[["AppType"], Mapping[str, Any]], - ) -> Callable[["AppType"], Mapping[str, Any]]: ... - - @overload - def after_startup( - self, - func: Callable[["AppType"], Awaitable[Mapping[str, Any]]], - ) -> Callable[["AppType"], Awaitable[Mapping[str, Any]]]: ... - - @overload - def after_startup( - self, - func: Callable[["AppType"], None], - ) -> Callable[["AppType"], None]: ... - - @overload - def after_startup( - self, - func: Callable[["AppType"], Awaitable[None]], - ) -> Callable[["AppType"], Awaitable[None]]: ... - - def after_startup( - self, - func: Union[ - Callable[["AppType"], Mapping[str, Any]], - Callable[["AppType"], Awaitable[Mapping[str, Any]]], - Callable[["AppType"], None], - Callable[["AppType"], Awaitable[None]], - ], - ) -> Union[ - Callable[["AppType"], Mapping[str, Any]], - Callable[["AppType"], Awaitable[Mapping[str, Any]]], - Callable[["AppType"], None], - Callable[["AppType"], Awaitable[None]], - ]: - """Register a function to be executed after startup.""" - self._after_startup_hooks.append(to_async(func)) - return func - - @overload - def on_broker_shutdown( - self, - func: Callable[["AppType"], None], - ) -> Callable[["AppType"], None]: ... - - @overload - def on_broker_shutdown( - self, - func: Callable[["AppType"], Awaitable[None]], - ) -> Callable[["AppType"], Awaitable[None]]: ... - - def on_broker_shutdown( - self, - func: Union[ - Callable[["AppType"], None], - Callable[["AppType"], Awaitable[None]], - ], - ) -> Union[ - Callable[["AppType"], None], - Callable[["AppType"], Awaitable[None]], - ]: - """Register a function to be executed before broker stop.""" - self._on_shutdown_hooks.append(to_async(func)) - return func - - @abstractmethod - def publisher(self) -> "PublisherProto[MsgType]": - """Create Publisher object.""" - raise NotImplementedError() - - def _asyncapi_router(self, schema_url: Optional[str]) -> Optional[APIRouter]: - """Creates an API router for serving AsyncAPI documentation.""" - if not self.include_in_schema or not schema_url: - return None - - def download_app_json_schema() -> Response: - assert ( # nosec B101 - self.schema - ), "You need to run application lifespan at first" - - return Response( - content=json.dumps(self.schema.to_jsonable(), indent=2), - headers={"Content-Type": "application/octet-stream"}, - ) - - def download_app_yaml_schema() -> Response: - assert ( # nosec B101 - self.schema - ), "You need to run application lifespan at first" - - return Response( - content=self.schema.to_yaml(), - headers={ - "Content-Type": "application/octet-stream", - }, - ) - - def serve_asyncapi_schema( - sidebar: bool = True, - info: bool = True, - servers: bool = True, - operations: bool = True, - messages: bool = True, - schemas: bool = True, - errors: bool = True, - expandMessageExamples: bool = True, - ) -> HTMLResponse: - """Serve the AsyncAPI schema as an HTML response.""" - assert ( # nosec B101 - self.schema - ), "You need to run application lifespan at first" - - return HTMLResponse( - content=get_asyncapi_html( - self.schema, - sidebar=sidebar, - info=info, - servers=servers, - operations=operations, - messages=messages, - schemas=schemas, - errors=errors, - expand_message_examples=expandMessageExamples, - title=self.schema.info.title, - ) - ) - - docs_router = APIRouter( - prefix=self.prefix, - tags=["asyncapi"], - redirect_slashes=self.redirect_slashes, - default=self.default, - deprecated=self.deprecated, - ) - docs_router.get(schema_url)(serve_asyncapi_schema) - docs_router.get(f"{schema_url}.json")(download_app_json_schema) - docs_router.get(f"{schema_url}.yaml")(download_app_yaml_schema) - return docs_router - - def include_router( # type: ignore[override] - self, - router: Union["StreamRouter[MsgType]", "BrokerRouter[MsgType]"], - *, - prefix: str = "", - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence["params.Depends"]] = None, - default_response_class: Type[Response] = Default(JSONResponse), - responses: Optional[Dict[Union[int, str], "AnyDict"]] = None, - callbacks: Optional[List["BaseRoute"]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - generate_unique_id_function: Callable[["APIRoute"], str] = Default( - generate_unique_id - ), - ) -> None: - """Includes a router in the API.""" - if isinstance(router, BrokerRouter): - for sub in router._subscribers.values(): - sub._call_decorators = ( # type: ignore[attr-defined] - self._add_api_mq_route( - dependencies=(), - response_model=Default(None), - response_model_include=None, - response_model_exclude=None, - response_model_by_alias=True, - response_model_exclude_unset=False, - response_model_exclude_defaults=False, - response_model_exclude_none=False, - ), - ) - - self.broker.include_router(router) - return - - if isinstance(router, StreamRouter): # pragma: no branch - router.lifespan_context = fake_context - self.broker.include_router(router.broker) - router.fastapi_config = self.fastapi_config - - super().include_router( - router=router, - prefix=prefix, - tags=tags, - dependencies=dependencies, - default_response_class=default_response_class, - responses=responses, - callbacks=callbacks, - deprecated=deprecated, - include_in_schema=include_in_schema, - generate_unique_id_function=generate_unique_id_function, - ) diff --git a/faststream/broker/message.py b/faststream/broker/message.py deleted file mode 100644 index c22b8ed199..0000000000 --- a/faststream/broker/message.py +++ /dev/null @@ -1,164 +0,0 @@ -import json -from contextlib import suppress -from dataclasses import dataclass, field -from enum import Enum -from typing import ( - TYPE_CHECKING, - Any, - Generic, - List, - Optional, - Sequence, - Tuple, - TypeVar, - Union, - cast, -) -from uuid import uuid4 - -from typing_extensions import deprecated - -from faststream._compat import dump_json, json_loads -from faststream.constants import ContentTypes -from faststream.types import EMPTY - -if TYPE_CHECKING: - from faststream.types import AnyDict, DecodedMessage, SendableMessage - -# prevent circular imports -MsgType = TypeVar("MsgType") - - -class AckStatus(str, Enum): - acked = "acked" - nacked = "nacked" - rejected = "rejected" - - -class SourceType(str, Enum): - Consume = "Consume" - """Message consumed by basic subscriber flow.""" - - Response = "Response" - """RPC response consumed.""" - - -def gen_cor_id() -> str: - """Generate random string to use as ID.""" - return str(uuid4()) - - -@dataclass -class StreamMessage(Generic[MsgType]): - """Generic class to represent a stream message.""" - - raw_message: "MsgType" - - body: Union[bytes, Any] - headers: "AnyDict" = field(default_factory=dict) - batch_headers: List["AnyDict"] = field(default_factory=list) - path: "AnyDict" = field(default_factory=dict) - - content_type: Optional[str] = None - reply_to: str = "" - message_id: str = field(default_factory=gen_cor_id) # pragma: no cover - correlation_id: str = field( - default_factory=gen_cor_id # pragma: no cover - ) - - processed: bool = field(default=False, init=False) - committed: Optional[AckStatus] = field(default=None, init=False) - _source_type: SourceType = field(default=SourceType.Consume) - _decoded_body: Optional["DecodedMessage"] = field(default=None, init=False) - - async def ack(self) -> None: - if not self.committed: - self.committed = AckStatus.acked - - async def nack(self) -> None: - if not self.committed: - self.committed = AckStatus.nacked - - async def reject(self) -> None: - if not self.committed: - self.committed = AckStatus.rejected - - async def decode(self) -> Optional["DecodedMessage"]: - """Serialize the message by lazy decoder.""" - # TODO: make it lazy after `decoded_body` removed - return self._decoded_body - - @property - @deprecated( - "Deprecated in **FastStream 0.5.19**. " - "Please, use `decode` lazy method instead. " - "Argument will be removed in **FastStream 0.6.0**.", - category=DeprecationWarning, - stacklevel=1, - ) - def decoded_body(self) -> Optional["DecodedMessage"]: - return self._decoded_body - - @decoded_body.setter - @deprecated( - "Deprecated in **FastStream 0.5.19**. " - "Please, use `decode` lazy method instead. " - "Argument will be removed in **FastStream 0.6.0**.", - category=DeprecationWarning, - stacklevel=1, - ) - def decoded_body(self, value: Optional["DecodedMessage"]) -> None: - self._decoded_body = value - - -def decode_message(message: "StreamMessage[Any]") -> "DecodedMessage": - """Decodes a message.""" - body: Any = getattr(message, "body", message) - m: DecodedMessage = body - - if (content_type := getattr(message, "content_type", EMPTY)) is not EMPTY: - content_type = cast("Optional[str]", content_type) - - if not content_type: - with suppress(json.JSONDecodeError, UnicodeDecodeError): - m = json_loads(body) - - elif ContentTypes.text.value in content_type: - m = body.decode() - - elif ContentTypes.json.value in content_type: - m = json_loads(body) - - else: - with suppress(json.JSONDecodeError, UnicodeDecodeError): - m = json_loads(body) - - return m - - -def encode_message( - msg: Union[Sequence["SendableMessage"], "SendableMessage"], -) -> Tuple[bytes, Optional[str]]: - """Encodes a message.""" - if msg is None: - return ( - b"", - None, - ) - - if isinstance(msg, bytes): - return ( - msg, - None, - ) - - if isinstance(msg, str): - return ( - msg.encode(), - ContentTypes.text.value, - ) - - return ( - dump_json(msg), - ContentTypes.json.value, - ) diff --git a/faststream/broker/middlewares/__init__.py b/faststream/broker/middlewares/__init__.py deleted file mode 100644 index c10aa33c3d..0000000000 --- a/faststream/broker/middlewares/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from faststream.broker.middlewares.base import BaseMiddleware -from faststream.broker.middlewares.exception import ExceptionMiddleware - -__all__ = ("BaseMiddleware", "ExceptionMiddleware") diff --git a/faststream/broker/middlewares/base.py b/faststream/broker/middlewares/base.py deleted file mode 100644 index 5710c8ec1c..0000000000 --- a/faststream/broker/middlewares/base.py +++ /dev/null @@ -1,115 +0,0 @@ -from typing import TYPE_CHECKING, Any, Optional, Type - -from typing_extensions import Self - -if TYPE_CHECKING: - from types import TracebackType - - from faststream.broker.message import StreamMessage - from faststream.types import AsyncFunc, AsyncFuncAny - - -class BaseMiddleware: - """A base middleware class.""" - - def __init__(self, msg: Optional[Any] = None) -> None: - self.msg = msg - - async def on_receive(self) -> None: - """Hook to call on message receive.""" - pass - - async def after_processed( - self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> Optional[bool]: - """Asynchronously called after processing.""" - return False - - async def __aenter__(self) -> Self: - await self.on_receive() - return self - - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> Optional[bool]: - """Exit the asynchronous context manager.""" - return await self.after_processed(exc_type, exc_val, exc_tb) - - async def on_consume( - self, - msg: "StreamMessage[Any]", - ) -> "StreamMessage[Any]": - """Asynchronously consumes a message.""" - return msg - - async def after_consume(self, err: Optional[Exception]) -> None: - """A function to handle the result of consuming a resource asynchronously.""" - if err is not None: - raise err - - async def consume_scope( - self, - call_next: "AsyncFuncAny", - msg: "StreamMessage[Any]", - ) -> Any: - """Asynchronously consumes a message and returns an asynchronous iterator of decoded messages.""" - err: Optional[Exception] = None - try: - result = await call_next(await self.on_consume(msg)) - - except Exception as e: - err = e - - else: - return result - - finally: - await self.after_consume(err) - - async def on_publish( - self, - msg: Any, - *args: Any, - **kwargs: Any, - ) -> Any: - """Asynchronously handle a publish event.""" - return msg - - async def after_publish( - self, - err: Optional[Exception], - ) -> None: - """Asynchronous function to handle the after publish event.""" - if err is not None: - raise err - - async def publish_scope( - self, - call_next: "AsyncFunc", - msg: Any, - *args: Any, - **kwargs: Any, - ) -> Any: - """Publish a message and return an async iterator.""" - err: Optional[Exception] = None - try: - result = await call_next( - await self.on_publish(msg, *args, **kwargs), - *args, - **kwargs, - ) - - except Exception as e: - err = e - - else: - return result - - finally: - await self.after_publish(err) diff --git a/faststream/broker/middlewares/exception.py b/faststream/broker/middlewares/exception.py deleted file mode 100644 index 2187372e8e..0000000000 --- a/faststream/broker/middlewares/exception.py +++ /dev/null @@ -1,212 +0,0 @@ -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - ContextManager, - Dict, - List, - NoReturn, - Optional, - Tuple, - Type, - Union, - cast, - overload, -) - -from typing_extensions import Literal, TypeAlias - -from faststream.broker.middlewares.base import BaseMiddleware -from faststream.exceptions import IgnoredException -from faststream.utils import apply_types, context -from faststream.utils.functions import sync_fake_context, to_async - -if TYPE_CHECKING: - from types import TracebackType - - from faststream.broker.message import StreamMessage - from faststream.types import AsyncFuncAny - - -GeneralExceptionHandler: TypeAlias = Union[ - Callable[..., None], Callable[..., Awaitable[None]] -] -PublishingExceptionHandler: TypeAlias = Callable[..., "Any"] - -CastedGeneralExceptionHandler: TypeAlias = Callable[..., Awaitable[None]] -CastedPublishingExceptionHandler: TypeAlias = Callable[..., Awaitable["Any"]] -CastedHandlers: TypeAlias = List[ - Tuple[ - Type[Exception], - CastedGeneralExceptionHandler, - ] -] -CastedPublishingHandlers: TypeAlias = List[ - Tuple[ - Type[Exception], - CastedPublishingExceptionHandler, - ] -] - - -class BaseExceptionMiddleware(BaseMiddleware): - def __init__( - self, - handlers: CastedHandlers, - publish_handlers: CastedPublishingHandlers, - msg: Optional[Any] = None, - ) -> None: - super().__init__(msg) - self._handlers = handlers - self._publish_handlers = publish_handlers - - async def consume_scope( - self, - call_next: "AsyncFuncAny", - msg: "StreamMessage[Any]", - ) -> Any: - try: - return await call_next(await self.on_consume(msg)) - - except Exception as exc: - exc_type = type(exc) - - for handler_type, handler in self._publish_handlers: - if issubclass(exc_type, handler_type): - return await handler(exc) - - raise exc - - async def after_processed( - self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> Optional[bool]: - if exc_type: - for handler_type, handler in self._handlers: - if issubclass(exc_type, handler_type): - # TODO: remove it after context will be moved to middleware - # In case parser/decoder error occurred - scope: ContextManager[Any] - if not context.get_local("message"): - scope = context.scope("message", self.msg) - else: - scope = sync_fake_context() - - with scope: - await handler(exc_val) - - return True - - return False - - return None - - -class ExceptionMiddleware: - __slots__ = ("_handlers", "_publish_handlers") - - _handlers: CastedHandlers - _publish_handlers: CastedPublishingHandlers - - def __init__( - self, - handlers: Optional[ - Dict[ - Type[Exception], - GeneralExceptionHandler, - ] - ] = None, - publish_handlers: Optional[ - Dict[ - Type[Exception], - PublishingExceptionHandler, - ] - ] = None, - ) -> None: - self._handlers: CastedHandlers = [ - (IgnoredException, ignore_handler), - *( - ( - exc_type, - apply_types( - cast("Callable[..., Awaitable[None]]", to_async(handler)) - ), - ) - for exc_type, handler in (handlers or {}).items() - ), - ] - - self._publish_handlers: CastedPublishingHandlers = [ - (IgnoredException, ignore_handler), - *( - (exc_type, apply_types(to_async(handler))) - for exc_type, handler in (publish_handlers or {}).items() - ), - ] - - @overload - def add_handler( - self, - exc: Type[Exception], - publish: Literal[False] = False, - ) -> Callable[[GeneralExceptionHandler], GeneralExceptionHandler]: ... - - @overload - def add_handler( - self, - exc: Type[Exception], - publish: Literal[True], - ) -> Callable[[PublishingExceptionHandler], PublishingExceptionHandler]: ... - - def add_handler( - self, - exc: Type[Exception], - publish: bool = False, - ) -> Union[ - Callable[[GeneralExceptionHandler], GeneralExceptionHandler], - Callable[[PublishingExceptionHandler], PublishingExceptionHandler], - ]: - if publish: - - def pub_wrapper( - func: PublishingExceptionHandler, - ) -> PublishingExceptionHandler: - self._publish_handlers.append( - ( - exc, - apply_types(to_async(func)), - ) - ) - return func - - return pub_wrapper - - else: - - def default_wrapper( - func: GeneralExceptionHandler, - ) -> GeneralExceptionHandler: - self._handlers.append( - ( - exc, - apply_types(to_async(func)), - ) - ) - return func - - return default_wrapper - - def __call__(self, msg: Optional[Any]) -> BaseExceptionMiddleware: - """Real middleware runtime constructor.""" - return BaseExceptionMiddleware( - handlers=self._handlers, - publish_handlers=self._publish_handlers, - msg=msg, - ) - - -async def ignore_handler(exception: IgnoredException) -> NoReturn: - raise exception diff --git a/faststream/broker/middlewares/logging.py b/faststream/broker/middlewares/logging.py deleted file mode 100644 index 11b818f85f..0000000000 --- a/faststream/broker/middlewares/logging.py +++ /dev/null @@ -1,74 +0,0 @@ -import logging -from typing import TYPE_CHECKING, Any, Optional, Type - -from typing_extensions import Self - -from faststream.broker.middlewares.base import BaseMiddleware -from faststream.exceptions import IgnoredException -from faststream.utils.context.repository import context - -if TYPE_CHECKING: - from types import TracebackType - - from faststream.broker.message import StreamMessage - from faststream.types import LoggerProto - - -class CriticalLogMiddleware(BaseMiddleware): - """A middleware class for logging critical errors.""" - - def __init__( - self, - logger: Optional["LoggerProto"], - log_level: int, - ) -> None: - """Initialize the class.""" - self.logger = logger - self.log_level = log_level - - def __call__(self, msg: Optional[Any]) -> Self: - """Call the object with a message.""" - self.msg = msg - return self - - async def on_consume( - self, - msg: "StreamMessage[Any]", - ) -> "StreamMessage[Any]": - if self.logger is not None: - c = context.get_local("log_context", {}) - self.logger.log(self.log_level, "Received", extra=c) - - return await super().on_consume(msg) - - async def after_processed( - self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> bool: - """Asynchronously called after processing.""" - if self.logger is not None: - c = context.get_local("log_context", {}) - - if exc_type: - if issubclass(exc_type, IgnoredException): - self.logger.log( - self.log_level, - exc_val, - extra=c, - ) - else: - self.logger.log( - logging.ERROR, - f"{exc_type.__name__}: {exc_val}", - exc_info=exc_val, - extra=c, - ) - - self.logger.log(self.log_level, "Processed", extra=c) - - await super().after_processed(exc_type, exc_val, exc_tb) - - # Exception was not processed - return False diff --git a/faststream/broker/proto.py b/faststream/broker/proto.py deleted file mode 100644 index c1083a92ba..0000000000 --- a/faststream/broker/proto.py +++ /dev/null @@ -1,12 +0,0 @@ -from abc import abstractmethod -from typing import Hashable, Protocol - - -class SetupAble(Protocol): - @abstractmethod - def setup(self) -> None: ... - - -class EndpointProto(SetupAble, Hashable, Protocol): - @abstractmethod - def add_prefix(self, prefix: str) -> None: ... diff --git a/faststream/broker/publisher/fake.py b/faststream/broker/publisher/fake.py deleted file mode 100644 index 83d681726b..0000000000 --- a/faststream/broker/publisher/fake.py +++ /dev/null @@ -1,59 +0,0 @@ -from functools import partial -from itertools import chain -from typing import TYPE_CHECKING, Any, Optional, Sequence - -from faststream.broker.publisher.proto import BasePublisherProto - -if TYPE_CHECKING: - from faststream.broker.types import PublisherMiddleware - from faststream.types import AnyDict, AsyncFunc, SendableMessage - - -class FakePublisher(BasePublisherProto): - """Publisher Interface implementation to use as RPC or REPLY TO publisher.""" - - def __init__( - self, - method: "AsyncFunc", - *, - publish_kwargs: "AnyDict", - middlewares: Sequence["PublisherMiddleware"] = (), - ) -> None: - """Initialize an object.""" - self.method = method - self.publish_kwargs = publish_kwargs - self.middlewares = middlewares - - async def publish( - self, - message: "SendableMessage", - *, - correlation_id: Optional[str] = None, - _extra_middlewares: Sequence["PublisherMiddleware"] = (), - **kwargs: Any, - ) -> Any: - """Publish a message.""" - publish_kwargs = { - "correlation_id": correlation_id, - **self.publish_kwargs, - **kwargs, - } - - call: AsyncFunc = self.method - for m in chain(_extra_middlewares, self.middlewares): - call = partial(m, call) - - return await call(message, **publish_kwargs) - - async def request( - self, - message: "SendableMessage", - /, - *, - correlation_id: Optional[str] = None, - _extra_middlewares: Sequence["PublisherMiddleware"] = (), - ) -> Any: - raise NotImplementedError( - "`FakePublisher` can be used only to publish " - "a response for `reply-to` or `RPC` messages." - ) diff --git a/faststream/broker/publisher/proto.py b/faststream/broker/publisher/proto.py deleted file mode 100644 index 9bb1a7be97..0000000000 --- a/faststream/broker/publisher/proto.py +++ /dev/null @@ -1,115 +0,0 @@ -from abc import abstractmethod -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Generic, - Optional, - Protocol, - Sequence, -) - -from typing_extensions import override - -from faststream.asyncapi.proto import AsyncAPIProto -from faststream.broker.proto import EndpointProto -from faststream.broker.types import MsgType - -if TYPE_CHECKING: - from faststream.broker.types import ( - AsyncCallable, - BrokerMiddleware, - P_HandlerParams, - PublisherMiddleware, - T_HandlerReturn, - ) - from faststream.types import SendableMessage - - -class ProducerProto(Protocol): - _parser: "AsyncCallable" - _decoder: "AsyncCallable" - - @abstractmethod - async def publish( - self, - message: "SendableMessage", - /, - *, - correlation_id: Optional[str] = None, - ) -> Optional[Any]: - """Publishes a message asynchronously.""" - ... - - @abstractmethod - async def request( - self, - message: "SendableMessage", - /, - *, - correlation_id: Optional[str] = None, - ) -> Any: - """Publishes a message synchronously.""" - ... - - -class BasePublisherProto(Protocol): - @abstractmethod - async def publish( - self, - message: "SendableMessage", - /, - *, - correlation_id: Optional[str] = None, - _extra_middlewares: Sequence["PublisherMiddleware"] = (), - ) -> Optional[Any]: - """Publishes a message asynchronously.""" - ... - - @abstractmethod - async def request( - self, - message: "SendableMessage", - /, - *, - correlation_id: Optional[str] = None, - _extra_middlewares: Sequence["PublisherMiddleware"] = (), - ) -> Optional[Any]: - """Publishes a message synchronously.""" - ... - - -class PublisherProto( - AsyncAPIProto, - EndpointProto, - BasePublisherProto, - Generic[MsgType], -): - schema_: Any - - _broker_middlewares: Sequence["BrokerMiddleware[MsgType]"] - _middlewares: Sequence["PublisherMiddleware"] - _producer: Optional["ProducerProto"] - - @abstractmethod - def add_middleware(self, middleware: "BrokerMiddleware[MsgType]") -> None: ... - - @staticmethod - @abstractmethod - def create() -> "PublisherProto[MsgType]": - """Abstract factory to create a real Publisher.""" - ... - - @override - @abstractmethod - def setup( # type: ignore[override] - self, - *, - producer: Optional["ProducerProto"], - ) -> None: ... - - @abstractmethod - def __call__( - self, - func: "Callable[P_HandlerParams, T_HandlerReturn]", - ) -> "Callable[P_HandlerParams, T_HandlerReturn]": ... diff --git a/faststream/broker/publisher/usecase.py b/faststream/broker/publisher/usecase.py deleted file mode 100644 index 6ac51053c9..0000000000 --- a/faststream/broker/publisher/usecase.py +++ /dev/null @@ -1,174 +0,0 @@ -from abc import ABC -from inspect import unwrap -from typing import ( - TYPE_CHECKING, - Any, - Callable, - List, - Optional, - Sequence, - Tuple, -) -from unittest.mock import MagicMock - -from fast_depends._compat import create_model, get_config_base -from fast_depends.core import CallModel, build_call_model -from typing_extensions import Annotated, Doc, override - -from faststream.asyncapi.abc import AsyncAPIOperation -from faststream.asyncapi.message import get_response_schema -from faststream.asyncapi.utils import to_camelcase -from faststream.broker.publisher.proto import PublisherProto -from faststream.broker.types import ( - MsgType, - P_HandlerParams, - T_HandlerReturn, -) -from faststream.broker.wrapper.call import HandlerCallWrapper - -if TYPE_CHECKING: - from faststream.broker.publisher.proto import ProducerProto - from faststream.broker.types import ( - BrokerMiddleware, - PublisherMiddleware, - ) - from faststream.types import AnyDict - - -class PublisherUsecase( - ABC, - AsyncAPIOperation, - PublisherProto[MsgType], -): - """A base class for publishers in an asynchronous API.""" - - mock: Optional[MagicMock] - calls: List[Callable[..., Any]] - - def __init__( - self, - *, - broker_middlewares: Annotated[ - Sequence["BrokerMiddleware[MsgType]"], - Doc("Top-level middlewares to use in direct `.publish` call."), - ], - middlewares: Annotated[ - Sequence["PublisherMiddleware"], - Doc("Publisher middlewares."), - ], - # AsyncAPI args - schema_: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type" - "Should be any python-native object annotation or `pydantic.BaseModel`." - ), - ], - title_: Annotated[ - Optional[str], - Doc("AsyncAPI object title."), - ], - description_: Annotated[ - Optional[str], - Doc("AsyncAPI object description."), - ], - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ], - ) -> None: - self.calls = [] - self._middlewares = middlewares - self._broker_middlewares = broker_middlewares - self._producer = None - - self._fake_handler = False - self.mock = None - - # AsyncAPI - self.title_ = title_ - self.description_ = description_ - self.include_in_schema = include_in_schema - self.schema_ = schema_ - - def add_middleware(self, middleware: "BrokerMiddleware[MsgType]") -> None: - self._broker_middlewares = (*self._broker_middlewares, middleware) - - @override - def setup( # type: ignore[override] - self, - *, - producer: Optional["ProducerProto"], - ) -> None: - self._producer = producer - - def set_test( - self, - *, - mock: Annotated[ - MagicMock, - Doc("Mock object to check in tests."), - ], - with_fake: Annotated[ - bool, - Doc("Whetevet publisher's fake subscriber created or not."), - ], - ) -> None: - """Turn publisher to testing mode.""" - self.mock = mock - self._fake_handler = with_fake - - def reset_test(self) -> None: - """Turn off publisher's testing mode.""" - self._fake_handler = False - self.mock = None - - def __call__( - self, - func: Callable[P_HandlerParams, T_HandlerReturn], - ) -> HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]: - """Decorate user's function by current publisher.""" - handler_call = HandlerCallWrapper[ - MsgType, - P_HandlerParams, - T_HandlerReturn, - ](func) - handler_call._publishers.append(self) - self.calls.append(handler_call._original_call) - return handler_call - - def get_payloads(self) -> List[Tuple["AnyDict", str]]: - payloads: List[Tuple[AnyDict, str]] = [] - - if self.schema_: - params = {"response__": (self.schema_, ...)} - - call_model: CallModel[Any, Any] = CallModel( - call=lambda: None, - model=create_model("Fake"), - response_model=create_model( # type: ignore[call-overload] - "", - __config__=get_config_base(), - **params, - ), - params=params, - ) - - body = get_response_schema( - call_model, - prefix=f"{self.name}:Message", - ) - if body: # pragma: no branch - payloads.append((body, "")) - - else: - for call in self.calls: - call_model = build_call_model(call) - body = get_response_schema( - call_model, - prefix=f"{self.name}:Message", - ) - if body: - payloads.append((body, to_camelcase(unwrap(call).__name__))) - - return payloads diff --git a/faststream/broker/response.py b/faststream/broker/response.py deleted file mode 100644 index fb08993251..0000000000 --- a/faststream/broker/response.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import TYPE_CHECKING, Any, Optional, Union - -if TYPE_CHECKING: - from faststream.types import AnyDict - - -class Response: - def __init__( - self, - body: "Any", - *, - headers: Optional["AnyDict"] = None, - correlation_id: Optional[str] = None, - ) -> None: - """Initialize a handler.""" - self.body = body - self.headers = headers or {} - self.correlation_id = correlation_id - - def add_headers( - self, - extra_headers: "AnyDict", - *, - override: bool = True, - ) -> None: - if override: - self.headers = {**self.headers, **extra_headers} - else: - self.headers = {**extra_headers, **self.headers} - - def as_publish_kwargs(self) -> "AnyDict": - publish_options = { - "headers": self.headers, - "correlation_id": self.correlation_id, - } - return publish_options - - -def ensure_response(response: Union["Response", "Any"]) -> "Response": - if isinstance(response, Response): - return response - - return Response(response) diff --git a/faststream/broker/router.py b/faststream/broker/router.py deleted file mode 100644 index e9dcb399d0..0000000000 --- a/faststream/broker/router.py +++ /dev/null @@ -1,88 +0,0 @@ -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Iterable, - Optional, - Sequence, -) - -from faststream.broker.core.abc import ABCBroker -from faststream.broker.types import MsgType - -if TYPE_CHECKING: - from fast_depends.dependencies import Depends - - from faststream.broker.types import ( - BrokerMiddleware, - CustomCallable, - ) - from faststream.types import AnyDict - - -class ArgsContainer: - """Class to store any arguments.""" - - args: Iterable[Any] - kwargs: "AnyDict" - - def __init__( - self, - *args: Any, - **kwargs: Any, - ) -> None: - self.args = args - self.kwargs = kwargs - - -class SubscriberRoute(ArgsContainer): - """A generic class to represent a broker route.""" - - call: Callable[..., Any] - publishers: Iterable[Any] - - def __init__( - self, - call: Callable[..., Any], - *args: Any, - publishers: Iterable[ArgsContainer] = (), - **kwargs: Any, - ) -> None: - """Initialize a callable object with arguments and keyword arguments.""" - self.call = call - self.publishers = publishers - - super().__init__(*args, **kwargs) - - -class BrokerRouter(ABCBroker[MsgType]): - """A generic class representing a broker router.""" - - def __init__( - self, - *, - handlers: Iterable[SubscriberRoute], - # base options - prefix: str, - dependencies: Iterable["Depends"], - middlewares: Sequence["BrokerMiddleware[MsgType]"], - parser: Optional["CustomCallable"], - decoder: Optional["CustomCallable"], - include_in_schema: Optional[bool], - ) -> None: - super().__init__( - prefix=prefix, - dependencies=dependencies, - middlewares=middlewares, - parser=parser, - decoder=decoder, - include_in_schema=include_in_schema, - ) - - for h in handlers: - call = h.call - - for p in h.publishers: - call = self.publisher(*p.args, **p.kwargs)(call) - - self.subscriber(*h.args, **h.kwargs)(call) diff --git a/faststream/broker/schemas.py b/faststream/broker/schemas.py deleted file mode 100644 index 75b624df87..0000000000 --- a/faststream/broker/schemas.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Any, Optional, Type, TypeVar, Union, overload - -NameRequiredCls = TypeVar("NameRequiredCls", bound="NameRequired") - - -class NameRequired: - """Required name option object.""" - - def __eq__(self, __value: object) -> bool: - """Compares the current object with another object for equality.""" - if __value is None: - return False - - if not isinstance(__value, NameRequired): - return NotImplemented - - return self.name == __value.name - - def __init__(self, name: str) -> None: - self.name = name - - @overload - @classmethod - def validate( - cls: Type[NameRequiredCls], - value: Union[str, NameRequiredCls], - **kwargs: Any, - ) -> NameRequiredCls: ... - - @overload - @classmethod - def validate( - cls: Type[NameRequiredCls], - value: None, - **kwargs: Any, - ) -> None: ... - - @classmethod - def validate( - cls: Type[NameRequiredCls], - value: Union[str, NameRequiredCls, None], - **kwargs: Any, - ) -> Optional[NameRequiredCls]: - """Factory to create object.""" - if value is not None and isinstance(value, str): - value = cls(value, **kwargs) - return value diff --git a/faststream/broker/subscriber/call_item.py b/faststream/broker/subscriber/call_item.py deleted file mode 100644 index dfd688150f..0000000000 --- a/faststream/broker/subscriber/call_item.py +++ /dev/null @@ -1,178 +0,0 @@ -from functools import partial -from inspect import unwrap -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Generic, - Iterable, - Optional, - Reversible, - Sequence, - cast, -) - -from typing_extensions import override - -from faststream.broker.proto import SetupAble -from faststream.broker.types import MsgType -from faststream.exceptions import IgnoredException, SetupError - -if TYPE_CHECKING: - from fast_depends.dependencies import Depends - - from faststream.broker.message import StreamMessage - from faststream.broker.types import ( - AsyncCallable, - AsyncFilter, - CustomCallable, - SubscriberMiddleware, - ) - from faststream.broker.wrapper.call import HandlerCallWrapper - from faststream.types import AsyncFuncAny, Decorator - - -class HandlerItem(SetupAble, Generic[MsgType]): - """A class representing handler overloaded item.""" - - __slots__ = ( - "dependant", - "dependencies", - "filter", - "handler", - "item_decoder", - "item_middlewares", - "item_parser", - ) - - dependant: Optional[Any] - - def __init__( - self, - *, - handler: "HandlerCallWrapper[MsgType, ..., Any]", - filter: "AsyncFilter[StreamMessage[MsgType]]", - item_parser: Optional["CustomCallable"], - item_decoder: Optional["CustomCallable"], - item_middlewares: Sequence["SubscriberMiddleware[StreamMessage[MsgType]]"], - dependencies: Iterable["Depends"], - ) -> None: - self.handler = handler - self.filter = filter - self.item_parser = item_parser - self.item_decoder = item_decoder - self.item_middlewares = item_middlewares - self.dependencies = dependencies - self.dependant = None - - def __repr__(self) -> str: - filter_call = unwrap(self.filter) - filter_name = getattr(filter_call, "__name__", str(filter_call)) - return f"<'{self.call_name}': filter='{filter_name}'>" - - @override - def setup( # type: ignore[override] - self, - *, - parser: "AsyncCallable", - decoder: "AsyncCallable", - broker_dependencies: Iterable["Depends"], - apply_types: bool, - is_validate: bool, - _get_dependant: Optional[Callable[..., Any]], - _call_decorators: Reversible["Decorator"], - ) -> None: - if self.dependant is None: - self.item_parser = parser - self.item_decoder = decoder - - dependencies = (*broker_dependencies, *self.dependencies) - - dependant = self.handler.set_wrapped( - apply_types=apply_types, - is_validate=is_validate, - dependencies=dependencies, - _get_dependant=_get_dependant, - _call_decorators=_call_decorators, - ) - - if _get_dependant is None: - self.dependant = dependant - else: - self.dependant = _get_dependant( - self.handler._original_call, - dependencies, - ) - - @property - def call_name(self) -> str: - """Returns the name of the original call.""" - if self.handler is None: - return "" - - caller = unwrap(self.handler._original_call) - name = getattr(caller, "__name__", str(caller)) - return name - - @property - def description(self) -> Optional[str]: - """Returns the description of original call.""" - if self.handler is None: - return None - - caller = unwrap(self.handler._original_call) - description = getattr(caller, "__doc__", None) - return description - - async def is_suitable( - self, - msg: MsgType, - cache: Dict[Any, Any], - ) -> Optional["StreamMessage[MsgType]"]: - """Check is message suite for current filter.""" - if not (parser := cast("Optional[AsyncCallable]", self.item_parser)) or not ( - decoder := cast("Optional[AsyncCallable]", self.item_decoder) - ): - raise SetupError("You should setup `HandlerItem` at first.") - - message = cache[parser] = cast( - "StreamMessage[MsgType]", cache.get(parser) or await parser(msg) - ) - - message._decoded_body = cache[decoder] = cache.get(decoder) or await decoder( - message - ) - - if await self.filter(message): - return message - - return None - - async def call( - self, - /, - message: "StreamMessage[MsgType]", - _extra_middlewares: Iterable["SubscriberMiddleware[Any]"], - ) -> Any: - """Execute wrapped handler with consume middlewares.""" - call: AsyncFuncAny = self.handler.call_wrapped - - for middleware in chain(self.item_middlewares[::-1], _extra_middlewares): - call = partial(middleware, call) - - try: - result = await call(message) - - except (IgnoredException, SystemExit): - self.handler.trigger() - raise - - except Exception as e: - self.handler.trigger(error=e) - raise e - - else: - self.handler.trigger(result=result) - return result diff --git a/faststream/broker/subscriber/proto.py b/faststream/broker/subscriber/proto.py deleted file mode 100644 index be296eec17..0000000000 --- a/faststream/broker/subscriber/proto.py +++ /dev/null @@ -1,112 +0,0 @@ -from abc import abstractmethod -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, - Optional, - Sequence, -) - -from typing_extensions import Self, override - -from faststream.asyncapi.proto import AsyncAPIProto -from faststream.broker.proto import EndpointProto -from faststream.broker.types import MsgType -from faststream.broker.wrapper.proto import WrapperProto - -if TYPE_CHECKING: - from fast_depends.dependencies import Depends - - from faststream.broker.message import StreamMessage - from faststream.broker.publisher.proto import BasePublisherProto, ProducerProto - from faststream.broker.response import Response - from faststream.broker.subscriber.call_item import HandlerItem - from faststream.broker.types import ( - BrokerMiddleware, - CustomCallable, - Filter, - SubscriberMiddleware, - ) - from faststream.types import AnyDict, Decorator, LoggerProto - - -class SubscriberProto( - AsyncAPIProto, - EndpointProto, - WrapperProto[MsgType], -): - calls: List["HandlerItem[MsgType]"] - running: bool - - _broker_dependencies: Iterable["Depends"] - _broker_middlewares: Sequence["BrokerMiddleware[MsgType]"] - _producer: Optional["ProducerProto"] - - @abstractmethod - def add_middleware(self, middleware: "BrokerMiddleware[MsgType]") -> None: ... - - @abstractmethod - def get_log_context( - self, - msg: Optional["StreamMessage[MsgType]"], - /, - ) -> Dict[str, str]: ... - - @override - @abstractmethod - def setup( # type: ignore[override] - self, - *, - logger: Optional["LoggerProto"], - graceful_timeout: Optional[float], - broker_parser: Optional["CustomCallable"], - broker_decoder: Optional["CustomCallable"], - producer: Optional["ProducerProto"], - extra_context: "AnyDict", - # FastDepends options - apply_types: bool, - is_validate: bool, - _get_dependant: Optional[Callable[..., Any]], - _call_decorators: Iterable["Decorator"], - ) -> None: ... - - @abstractmethod - def _make_response_publisher( - self, - message: "StreamMessage[MsgType]", - ) -> Iterable["BasePublisherProto"]: ... - - @property - @abstractmethod - def call_name(self) -> str: ... - - @abstractmethod - async def start(self) -> None: ... - - @abstractmethod - async def close(self) -> None: ... - - @abstractmethod - async def consume(self, msg: MsgType) -> Any: ... - - @abstractmethod - async def process_message(self, msg: MsgType) -> "Response": ... - - @abstractmethod - async def get_one( - self, *, timeout: float = 5.0 - ) -> "Optional[StreamMessage[MsgType]]": ... - - @abstractmethod - def add_call( - self, - *, - filter_: "Filter[Any]", - parser_: "CustomCallable", - decoder_: "CustomCallable", - middlewares_: Sequence["SubscriberMiddleware[Any]"], - dependencies_: Iterable["Depends"], - ) -> Self: ... diff --git a/faststream/broker/subscriber/usecase.py b/faststream/broker/subscriber/usecase.py deleted file mode 100644 index 15e9e99589..0000000000 --- a/faststream/broker/subscriber/usecase.py +++ /dev/null @@ -1,499 +0,0 @@ -from abc import abstractmethod -from contextlib import AsyncExitStack -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Callable, - ContextManager, - Dict, - Iterable, - List, - Optional, - Sequence, - Tuple, - Union, - overload, -) - -from typing_extensions import Self, override - -from faststream.asyncapi.abc import AsyncAPIOperation -from faststream.asyncapi.message import parse_handler_params -from faststream.asyncapi.utils import to_camelcase -from faststream.broker.response import ensure_response -from faststream.broker.subscriber.call_item import HandlerItem -from faststream.broker.subscriber.proto import SubscriberProto -from faststream.broker.types import ( - MsgType, - P_HandlerParams, - T_HandlerReturn, -) -from faststream.broker.utils import MultiLock, get_watcher_context, resolve_custom_func -from faststream.broker.wrapper.call import HandlerCallWrapper -from faststream.exceptions import SetupError, StopConsume, SubscriberNotFound -from faststream.utils.context.repository import context -from faststream.utils.functions import sync_fake_context, to_async - -if TYPE_CHECKING: - from fast_depends.dependencies import Depends - - from faststream.broker.message import StreamMessage - from faststream.broker.middlewares import BaseMiddleware - from faststream.broker.publisher.proto import BasePublisherProto, ProducerProto - from faststream.broker.response import Response - from faststream.broker.types import ( - AsyncCallable, - BrokerMiddleware, - CustomCallable, - Filter, - SubscriberMiddleware, - ) - from faststream.types import AnyDict, Decorator, LoggerProto - - -class _CallOptions: - __slots__ = ( - "decoder", - "dependencies", - "filter", - "middlewares", - "parser", - ) - - def __init__( - self, - *, - filter: "Filter[Any]", - parser: Optional["CustomCallable"], - decoder: Optional["CustomCallable"], - middlewares: Sequence["SubscriberMiddleware[Any]"], - dependencies: Iterable["Depends"], - ) -> None: - self.filter = filter - self.parser = parser - self.decoder = decoder - self.middlewares = middlewares - self.dependencies = dependencies - - -class SubscriberUsecase( - AsyncAPIOperation, - SubscriberProto[MsgType], -): - """A class representing an asynchronous handler.""" - - lock: ContextManager[Any] - extra_watcher_options: "AnyDict" - extra_context: "AnyDict" - graceful_timeout: Optional[float] - logger: Optional["LoggerProto"] - - _broker_dependencies: Iterable["Depends"] - _call_options: Optional["_CallOptions"] - _call_decorators: Iterable["Decorator"] - - def __init__( - self, - *, - no_ack: bool, - no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[MsgType]"], - default_parser: "AsyncCallable", - default_decoder: "AsyncCallable", - # AsyncAPI information - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - """Initialize a new instance of the class.""" - self.calls = [] - - self._parser = default_parser - self._decoder = default_decoder - self._no_reply = no_reply - # Watcher args - self._no_ack = no_ack - self._retry = retry - - self._call_options = None - self._call_decorators = () - self.running = False - self.lock = sync_fake_context() - - # Setup in include - self._broker_dependencies = broker_dependencies - self._broker_middlewares = broker_middlewares - - # register in setup later - self._producer = None - self.graceful_timeout = None - self.extra_context = {} - self.extra_watcher_options = {} - - # AsyncAPI - self.title_ = title_ - self.description_ = description_ - self.include_in_schema = include_in_schema - - def add_middleware(self, middleware: "BrokerMiddleware[MsgType]") -> None: - self._broker_middlewares = (*self._broker_middlewares, middleware) - - @override - def setup( # type: ignore[override] - self, - *, - logger: Optional["LoggerProto"], - producer: Optional["ProducerProto"], - graceful_timeout: Optional[float], - extra_context: "AnyDict", - # broker options - broker_parser: Optional["CustomCallable"], - broker_decoder: Optional["CustomCallable"], - # dependant args - apply_types: bool, - is_validate: bool, - _get_dependant: Optional[Callable[..., Any]], - _call_decorators: Iterable["Decorator"], - ) -> None: - self.lock = MultiLock() - - self._producer = producer - self.graceful_timeout = graceful_timeout - self.extra_context = extra_context - self.logger = logger - - self.watcher = get_watcher_context(logger, self._no_ack, self._retry) - - for call in self.calls: - if parser := call.item_parser or broker_parser: - async_parser = resolve_custom_func(to_async(parser), self._parser) - else: - async_parser = self._parser - - if decoder := call.item_decoder or broker_decoder: - async_decoder = resolve_custom_func(to_async(decoder), self._decoder) - else: - async_decoder = self._decoder - - self._parser = async_parser - self._decoder = async_decoder - - call.setup( - parser=async_parser, - decoder=async_decoder, - apply_types=apply_types, - is_validate=is_validate, - _get_dependant=_get_dependant, - _call_decorators=(*self._call_decorators, *_call_decorators), - broker_dependencies=self._broker_dependencies, - ) - - call.handler.refresh(with_mock=False) - - @abstractmethod - async def start(self) -> None: - """Start the handler.""" - self.running = True - - @abstractmethod - async def close(self) -> None: - """Close the handler. - - Blocks event loop up to graceful_timeout seconds. - """ - self.running = False - if isinstance(self.lock, MultiLock): - await self.lock.wait_release(self.graceful_timeout) - - def add_call( - self, - *, - filter_: "Filter[Any]", - parser_: Optional["CustomCallable"], - decoder_: Optional["CustomCallable"], - middlewares_: Sequence["SubscriberMiddleware[Any]"], - dependencies_: Iterable["Depends"], - ) -> Self: - self._call_options = _CallOptions( - filter=filter_, - parser=parser_, - decoder=decoder_, - middlewares=middlewares_, - dependencies=dependencies_, - ) - return self - - @overload - def __call__( - self, - func: None = None, - *, - filter: Optional["Filter[Any]"] = None, - parser: Optional["CustomCallable"] = None, - decoder: Optional["CustomCallable"] = None, - middlewares: Sequence["SubscriberMiddleware[Any]"] = (), - dependencies: Iterable["Depends"] = (), - ) -> Callable[ - [Callable[P_HandlerParams, T_HandlerReturn]], - "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", - ]: ... - - @overload - def __call__( - self, - func: Callable[P_HandlerParams, T_HandlerReturn], - *, - filter: Optional["Filter[Any]"] = None, - parser: Optional["CustomCallable"] = None, - decoder: Optional["CustomCallable"] = None, - middlewares: Sequence["SubscriberMiddleware[Any]"] = (), - dependencies: Iterable["Depends"] = (), - ) -> "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]": ... - - def __call__( - self, - func: Optional[Callable[P_HandlerParams, T_HandlerReturn]] = None, - *, - filter: Optional["Filter[Any]"] = None, - parser: Optional["CustomCallable"] = None, - decoder: Optional["CustomCallable"] = None, - middlewares: Sequence["SubscriberMiddleware[Any]"] = (), - dependencies: Iterable["Depends"] = (), - ) -> Any: - if (options := self._call_options) is None: - raise SetupError( - "You can't create subscriber directly. Please, use `add_call` at first." - ) - - total_deps = (*options.dependencies, *dependencies) - total_middlewares = (*options.middlewares, *middlewares) - - def real_wrapper( - func: Callable[P_HandlerParams, T_HandlerReturn], - ) -> "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]": - handler = HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]( - func - ) - - self.calls.append( - HandlerItem[MsgType]( - handler=handler, - filter=to_async(filter or options.filter), - item_parser=parser or options.parser, - item_decoder=decoder or options.decoder, - item_middlewares=total_middlewares, - dependencies=total_deps, - ) - ) - - return handler - - if func is None: - return real_wrapper - - else: - return real_wrapper(func) - - async def consume(self, msg: MsgType) -> Any: - """Consume a message asynchronously.""" - if not self.running: - return None - - try: - return await self.process_message(msg) - - except StopConsume: - # Stop handler at StopConsume exception - await self.close() - - except SystemExit: - # Stop handler at `exit()` call - try: - await self.close() - finally: - # Ensure that app.exit() is called on a shutdown request - # even if the consumer close operation threw an error. - if app := context.get("app"): - app.exit() - - except Exception: # nosec B110 - # All other exceptions were logged by CriticalLogMiddleware - pass - - async def process_message(self, msg: MsgType) -> "Response": - """Execute all message processing stages.""" - async with AsyncExitStack() as stack: - stack.enter_context(self.lock) - - # Enter context before middlewares - for k, v in self.extra_context.items(): - stack.enter_context(context.scope(k, v)) - - stack.enter_context(context.scope("handler_", self)) - - # enter all middlewares - middlewares: List[BaseMiddleware] = [] - for base_m in self._broker_middlewares: - middleware = base_m(msg) - middlewares.append(middleware) - await middleware.__aenter__() - - cache: Dict[Any, Any] = {} - parsing_error: Optional[Exception] = None - for h in self.calls: - try: - message = await h.is_suitable(msg, cache) - except Exception as e: - parsing_error = e - break - - if message is not None: - # Acknowledgement scope - # TODO: move it to scope enter at `retry` option deprecation - await stack.enter_async_context( - self.watcher( - message, - **self.extra_watcher_options, - ) - ) - - stack.enter_context( - context.scope("log_context", self.get_log_context(message)) - ) - stack.enter_context(context.scope("message", message)) - - # Middlewares should be exited before scope release - for m in middlewares: - stack.push_async_exit(m.__aexit__) - - result_msg = ensure_response( - await h.call( - message=message, - # consumer middlewares - _extra_middlewares=( - m.consume_scope for m in middlewares[::-1] - ), - ) - ) - - if not result_msg.correlation_id: - result_msg.correlation_id = message.correlation_id - - for p in chain( - self.__get_response_publisher(message), - h.handler._publishers, - ): - await p.publish( - result_msg.body, - **result_msg.as_publish_kwargs(), - # publisher middlewares - _extra_middlewares=[ - m.publish_scope for m in middlewares[::-1] - ], - ) - - # Return data for tests - return result_msg - - # Suitable handler was not found or - # parsing/decoding exception occurred - for m in middlewares: - stack.push_async_exit(m.__aexit__) - - if parsing_error: - raise parsing_error - - else: - raise SubscriberNotFound(f"There is no suitable handler for {msg=}") - - # An error was raised and processed by some middleware - return ensure_response(None) - - def __get_response_publisher( - self, - message: "StreamMessage[MsgType]", - ) -> Iterable["BasePublisherProto"]: - if not message.reply_to or self._no_reply: - return () - - else: - return self._make_response_publisher(message) - - def get_log_context( - self, - message: Optional["StreamMessage[MsgType]"], - ) -> Dict[str, str]: - """Generate log context.""" - return { - "message_id": getattr(message, "message_id", ""), - } - - # AsyncAPI methods - - @property - def call_name(self) -> str: - """Returns the name of the handler call.""" - if not self.calls: - return "Subscriber" - - if len(self.calls) == 1: - return to_camelcase(self.calls[0].call_name) - - return f"[{','.join(to_camelcase(c.call_name) for c in self.calls)}]" - - def get_description(self) -> Optional[str]: - """Returns the description of the handler.""" - if not self.calls: # pragma: no cover - return None - - if len(self.calls) == 1: - return self.calls[0].description - - return "\n".join( - f"{to_camelcase(h.call_name)}: {h.description}" for h in self.calls - ) - - def get_payloads(self) -> List[Tuple["AnyDict", str]]: - """Get the payloads of the handler.""" - payloads: List[Tuple[AnyDict, str]] = [] - - for h in self.calls: - if h.dependant is None: - raise SetupError("You should setup `Handler` at first.") - - body = parse_handler_params( - h.dependant, - prefix=f"{self.title_ or self.call_name}:Message", - ) - - payloads.append((body, to_camelcase(h.call_name))) - - if not self.calls: - payloads.append( - ( - { - "title": f"{self.title_ or self.call_name}:Message:Payload", - }, - to_camelcase(self.call_name), - ) - ) - - return payloads - - def _log( - self, - log_level: int, - message: str, - extra: Optional["AnyDict"] = None, - exc_info: Optional[Exception] = None, - ) -> None: - if self.logger is not None: - self.logger.log( - log_level, - message, - extra=extra, - exc_info=exc_info, - ) diff --git a/faststream/broker/types.py b/faststream/broker/types.py deleted file mode 100644 index 145a37418e..0000000000 --- a/faststream/broker/types.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import ( - Any, - Awaitable, - Callable, - Optional, - Protocol, - TypeVar, - Union, -) - -from typing_extensions import ParamSpec, TypeAlias - -from faststream.broker.message import StreamMessage -from faststream.broker.middlewares import BaseMiddleware -from faststream.types import AsyncFunc, AsyncFuncAny - -MsgType = TypeVar("MsgType") -StreamMsg = TypeVar("StreamMsg", bound=StreamMessage[Any]) -ConnectionType = TypeVar("ConnectionType") - - -SyncFilter: TypeAlias = Callable[[StreamMsg], bool] -AsyncFilter: TypeAlias = Callable[[StreamMsg], Awaitable[bool]] -Filter: TypeAlias = Union[ - SyncFilter[StreamMsg], - AsyncFilter[StreamMsg], -] - -SyncCallable: TypeAlias = Callable[ - [Any], - Any, -] -AsyncCallable: TypeAlias = Callable[ - [Any], - Awaitable[Any], -] -AsyncCustomCallable: TypeAlias = Union[ - AsyncCallable, - Callable[ - [Any, AsyncCallable], - Awaitable[Any], - ], -] -CustomCallable: TypeAlias = Union[ - AsyncCustomCallable, - SyncCallable, -] - -P_HandlerParams = ParamSpec("P_HandlerParams") -T_HandlerReturn = TypeVar("T_HandlerReturn") - - -AsyncWrappedHandlerCall: TypeAlias = Callable[ - [StreamMessage[MsgType]], - Awaitable[Optional[T_HandlerReturn]], -] -SyncWrappedHandlerCall: TypeAlias = Callable[ - [StreamMessage[MsgType]], - Optional[T_HandlerReturn], -] -WrappedHandlerCall: TypeAlias = Union[ - AsyncWrappedHandlerCall[MsgType, T_HandlerReturn], - SyncWrappedHandlerCall[MsgType, T_HandlerReturn], -] - - -BrokerMiddleware: TypeAlias = Callable[[Optional[MsgType]], BaseMiddleware] -SubscriberMiddleware: TypeAlias = Callable[ - [AsyncFuncAny, MsgType], - MsgType, -] - - -class PublisherMiddleware(Protocol): - """Publisher middleware interface.""" - - def __call__( - self, - call_next: AsyncFunc, - *__args: Any, - **__kwargs: Any, - ) -> Any: ... diff --git a/faststream/broker/utils.py b/faststream/broker/utils.py deleted file mode 100644 index 80be3be981..0000000000 --- a/faststream/broker/utils.py +++ /dev/null @@ -1,155 +0,0 @@ -import asyncio -import inspect -from contextlib import AsyncExitStack, suppress -from functools import partial -from typing import ( - TYPE_CHECKING, - Any, - AsyncContextManager, - Awaitable, - Callable, - Optional, - Sequence, - Type, - Union, - cast, -) - -import anyio -from typing_extensions import Self - -from faststream.broker.acknowledgement_watcher import WatcherContext, get_watcher -from faststream.broker.types import MsgType -from faststream.utils.functions import fake_context, return_input, to_async - -if TYPE_CHECKING: - from types import TracebackType - - from faststream.broker.message import StreamMessage - from faststream.broker.types import ( - AsyncCallable, - BrokerMiddleware, - CustomCallable, - SyncCallable, - ) - from faststream.types import LoggerProto - - -async def process_msg( - msg: Optional[MsgType], - middlewares: Sequence["BrokerMiddleware[MsgType]"], - parser: Callable[[MsgType], Awaitable["StreamMessage[MsgType]"]], - decoder: Callable[["StreamMessage[MsgType]"], "Any"], -) -> Optional["StreamMessage[MsgType]"]: - if msg is None: - return None - - async with AsyncExitStack() as stack: - return_msg: Callable[ - [StreamMessage[MsgType]], - Awaitable[StreamMessage[MsgType]], - ] = return_input - - for m in middlewares[::-1]: - mid = m(msg) - await stack.enter_async_context(mid) - return_msg = partial(mid.consume_scope, return_msg) - - parsed_msg = await parser(msg) - parsed_msg._decoded_body = await decoder(parsed_msg) - return await return_msg(parsed_msg) - - raise AssertionError("unreachable") - - -async def default_filter(msg: "StreamMessage[Any]") -> bool: - """A function to filter stream messages.""" - return not msg.processed - - -def get_watcher_context( - logger: Optional["LoggerProto"], - no_ack: bool, - retry: Union[bool, int], - **extra_options: Any, -) -> Callable[..., AsyncContextManager[None]]: - """Create Acknowledgement scope.""" - if no_ack: - return fake_context - - else: - return partial( - WatcherContext, - watcher=get_watcher(logger, retry), - logger=logger, - **extra_options, - ) - - -class MultiLock: - """A class representing a multi lock.""" - - def __init__(self) -> None: - """Initialize a new instance of the class.""" - self.queue: asyncio.Queue[None] = asyncio.Queue() - - def __enter__(self) -> Self: - """Enter the context.""" - self.acquire() - return self - - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional["TracebackType"], - ) -> None: - """Exit the context.""" - self.release() - - def acquire(self) -> None: - """Acquire lock.""" - self.queue.put_nowait(None) - - def release(self) -> None: - """Release lock.""" - with suppress(asyncio.QueueEmpty, ValueError): - self.queue.get_nowait() - self.queue.task_done() - - @property - def qsize(self) -> int: - """Return the size of the queue.""" - return self.queue.qsize() - - @property - def empty(self) -> bool: - """Return whether the queue is empty.""" - return self.queue.empty() - - async def wait_release(self, timeout: Optional[float] = None) -> None: - """Wait for the queue to be released. - - Using for graceful shutdown. - """ - if timeout: - with anyio.move_on_after(timeout): - await self.queue.join() - - -def resolve_custom_func( - custom_func: Optional["CustomCallable"], - default_func: "AsyncCallable", -) -> "AsyncCallable": - """Resolve a custom parser/decoder with default one.""" - if custom_func is None: - return default_func - - original_params = inspect.signature(custom_func).parameters - - if len(original_params) == 1: - return to_async(cast("Union[SyncCallable, AsyncCallable]", custom_func)) - - else: - name = tuple(original_params.items())[1][0] - return partial(to_async(custom_func), **{name: default_func}) diff --git a/faststream/broker/wrapper/call.py b/faststream/broker/wrapper/call.py deleted file mode 100644 index c4c1cb7895..0000000000 --- a/faststream/broker/wrapper/call.py +++ /dev/null @@ -1,206 +0,0 @@ -import asyncio -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Generic, - Iterable, - List, - Mapping, - Optional, - Reversible, - Sequence, - Union, -) -from unittest.mock import MagicMock - -import anyio -from fast_depends.core import CallModel, build_call_model -from fast_depends.use import _InjectWrapper, inject - -from faststream.broker.types import ( - MsgType, - P_HandlerParams, - T_HandlerReturn, -) -from faststream.exceptions import SetupError -from faststream.utils.functions import to_async - -if TYPE_CHECKING: - from fast_depends.dependencies import Depends - - from faststream.broker.message import StreamMessage - from faststream.broker.publisher.proto import PublisherProto - from faststream.types import Decorator - - -class HandlerCallWrapper(Generic[MsgType, P_HandlerParams, T_HandlerReturn]): - """A generic class to wrap handler calls.""" - - mock: Optional[MagicMock] - future: Optional["asyncio.Future[Any]"] - is_test: bool - - _wrapped_call: Optional[Callable[..., Awaitable[Any]]] - _original_call: Callable[P_HandlerParams, T_HandlerReturn] - _publishers: List["PublisherProto[MsgType]"] - - __slots__ = ( - "_original_call", - "_publishers", - "_wrapped_call", - "future", - "is_test", - "mock", - ) - - def __new__( - cls, - call: Union[ - "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", - Callable[P_HandlerParams, T_HandlerReturn], - ], - ) -> "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]": - """Create a new instance of the class.""" - if isinstance(call, cls): - return call - else: - return super().__new__(cls) - - def __init__( - self, - call: Callable[P_HandlerParams, T_HandlerReturn], - ) -> None: - """Initialize a handler.""" - if not isinstance(call, HandlerCallWrapper): - self._original_call = call - self._wrapped_call = None - self._publishers = [] - - self.mock = None - self.future = None - self.is_test = False - - def __call__( - self, - *args: P_HandlerParams.args, - **kwargs: P_HandlerParams.kwargs, - ) -> T_HandlerReturn: - """Calls the object as a function.""" - return self._original_call(*args, **kwargs) - - def call_wrapped( - self, - message: "StreamMessage[MsgType]", - ) -> Awaitable[Any]: - """Calls the wrapped function with the given message.""" - assert self._wrapped_call, "You should use `set_wrapped` first" # nosec B101 - if self.is_test: - assert self.mock # nosec B101 - self.mock(message._decoded_body) - return self._wrapped_call(message) - - async def wait_call(self, timeout: Optional[float] = None) -> None: - """Waits for a call with an optional timeout.""" - assert ( # nosec B101 - self.future is not None - ), "You can use this method only with TestClient" - with anyio.fail_after(timeout): - await self.future - - def set_test(self) -> None: - self.is_test = True - if self.mock is None: - self.mock = MagicMock() - self.refresh(with_mock=True) - - def reset_test(self) -> None: - self.is_test = False - self.mock = None - self.future = None - - def trigger( - self, - result: Any = None, - error: Optional[BaseException] = None, - ) -> None: - if not self.is_test: - return - - if self.future is None: - raise SetupError("You can use this method only with TestClient") - - if self.future.done(): - self.future = asyncio.Future() - - if error: - self.future.set_exception(error) - else: - self.future.set_result(result) - - def refresh(self, with_mock: bool = False) -> None: - if asyncio.events._get_running_loop() is not None: - self.future = asyncio.Future() - - if with_mock and self.mock is not None: - self.mock.reset_mock() - - def set_wrapped( - self, - *, - apply_types: bool, - is_validate: bool, - dependencies: Iterable["Depends"], - _get_dependant: Optional[Callable[..., Any]], - _call_decorators: Reversible["Decorator"], - ) -> Optional["CallModel[..., Any]"]: - call = self._original_call - for decor in reversed(_call_decorators): - call = decor(call) - self._original_call = call - - f: Callable[..., Awaitable[Any]] = to_async(call) - - dependent: Optional[CallModel[..., Any]] = None - if _get_dependant is None: - dependent = build_call_model( - f, - cast=is_validate, - extra_dependencies=dependencies, # type: ignore[arg-type] - ) - - if apply_types: - wrapper: _InjectWrapper[Any, Any] = inject(func=None) - f = wrapper(func=f, model=dependent) - - f = _wrap_decode_message( - func=f, - params_ln=len(dependent.flat_params), - ) - - self._wrapped_call = f - return dependent - - -def _wrap_decode_message( - func: Callable[..., Awaitable[T_HandlerReturn]], - params_ln: int, -) -> Callable[["StreamMessage[MsgType]"], Awaitable[T_HandlerReturn]]: - """Wraps a function to decode a message and pass it as an argument to the wrapped function.""" - - async def decode_wrapper(message: "StreamMessage[MsgType]") -> T_HandlerReturn: - """A wrapper function to decode and handle a message.""" - msg = await message.decode() - - if params_ln > 1: - if isinstance(msg, Mapping): - return await func(**msg) - elif isinstance(msg, Sequence): - return await func(*msg) - else: - return await func(msg) - - raise AssertionError("unreachable") - - return decode_wrapper diff --git a/faststream/broker/wrapper/proto.py b/faststream/broker/wrapper/proto.py deleted file mode 100644 index 9dfcb98af5..0000000000 --- a/faststream/broker/wrapper/proto.py +++ /dev/null @@ -1,80 +0,0 @@ -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Iterable, - Optional, - Protocol, - Sequence, - Union, - overload, -) - -from faststream.broker.types import ( - CustomCallable, - Filter, - MsgType, - P_HandlerParams, - SubscriberMiddleware, - T_HandlerReturn, -) - -if TYPE_CHECKING: - from fast_depends.dependencies import Depends - - from faststream.broker.wrapper.call import HandlerCallWrapper - - -class WrapperProto(Protocol[MsgType]): - """Annotation class to represent @subscriber return type.""" - - @overload - def __call__( - self, - func: None = None, - *, - filter: Optional["Filter[Any]"] = None, - parser: Optional["CustomCallable"] = None, - decoder: Optional["CustomCallable"] = None, - middlewares: Sequence["SubscriberMiddleware[Any]"] = (), - dependencies: Iterable["Depends"] = (), - ) -> Callable[ - [Callable[P_HandlerParams, T_HandlerReturn]], - "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", - ]: ... - - @overload - def __call__( - self, - func: Union[ - Callable[P_HandlerParams, T_HandlerReturn], - "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", - ], - *, - filter: Optional["Filter[Any]"] = None, - parser: Optional["CustomCallable"] = None, - decoder: Optional["CustomCallable"] = None, - middlewares: Sequence["SubscriberMiddleware[Any]"] = (), - dependencies: Iterable["Depends"] = (), - ) -> "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]": ... - - def __call__( - self, - func: Union[ - Callable[P_HandlerParams, T_HandlerReturn], - "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", - None, - ] = None, - *, - filter: Optional["Filter[Any]"] = None, - parser: Optional["CustomCallable"] = None, - decoder: Optional["CustomCallable"] = None, - middlewares: Sequence["SubscriberMiddleware[Any]"] = (), - dependencies: Iterable["Depends"] = (), - ) -> Union[ - "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", - Callable[ - [Callable[P_HandlerParams, T_HandlerReturn]], - "HandlerCallWrapper[MsgType, P_HandlerParams, T_HandlerReturn]", - ], - ]: ... diff --git a/faststream/cli/docs/app.py b/faststream/cli/docs/app.py deleted file mode 100644 index 1346543b1a..0000000000 --- a/faststream/cli/docs/app.py +++ /dev/null @@ -1,186 +0,0 @@ -import json -import sys -import warnings -from pathlib import Path -from typing import Optional, Sequence - -import typer - -from faststream._compat import json_dumps, model_parse -from faststream.asyncapi.generate import get_app_schema -from faststream.asyncapi.schema import Schema -from faststream.asyncapi.site import serve_app -from faststream.cli.utils.imports import import_from_string -from faststream.exceptions import INSTALL_WATCHFILES, INSTALL_YAML - -docs_app = typer.Typer(pretty_exceptions_short=True) - - -@docs_app.command(name="serve") -def serve( - app: str = typer.Argument( - ..., - help="[python_module:FastStream] or [asyncapi.yaml/.json] - path to your application or documentation.", - ), - host: str = typer.Option( - "localhost", - help="Documentation hosting address.", - ), - port: int = typer.Option( - 8000, - help="Documentation hosting port.", - ), - reload: bool = typer.Option( - False, - "--reload", - is_flag=True, - help="Restart documentation at directory files changes.", - ), - app_dir: str = typer.Option( - ".", - "--app-dir", - help=( - "Look for APP in the specified directory, by adding this to the PYTHONPATH." - " Defaults to the current working directory." - ), - ), - is_factory: bool = typer.Option( - False, - "--factory", - is_flag=True, - help="Treat APP as an application factory.", - ), -) -> None: - """Serve project AsyncAPI schema.""" - if ":" in app: - if app_dir: # pragma: no branch - sys.path.insert(0, app_dir) - - module, _ = import_from_string(app, is_factory=is_factory) - - module_parent = module.parent - extra_extensions: Sequence[str] = () - - else: - module_parent = Path.cwd() - schema_filepath = module_parent / app - extra_extensions = (schema_filepath.suffix,) - - if reload: - try: - from faststream.cli.supervisors.watchfiles import WatchReloader - - except ImportError: - warnings.warn(INSTALL_WATCHFILES, category=ImportWarning, stacklevel=1) - _parse_and_serve(app, host, port, is_factory) - - else: - WatchReloader( - target=_parse_and_serve, - args=(app, host, port, is_factory), - reload_dirs=(str(module_parent),), - extra_extensions=extra_extensions, - ).run() - - else: - _parse_and_serve(app, host, port, is_factory) - - -@docs_app.command(name="gen") -def gen( - app: str = typer.Argument( - ..., - help="[python_module:FastStream] - path to your application.", - ), - yaml: bool = typer.Option( - False, - "--yaml", - is_flag=True, - help="Generate `asyncapi.yaml` schema.", - ), - out: Optional[str] = typer.Option( - None, - help="Output filename.", - ), - app_dir: str = typer.Option( - ".", - "--app-dir", - help=( - "Look for APP in the specified directory, by adding this to the PYTHONPATH." - " Defaults to the current working directory." - ), - ), - is_factory: bool = typer.Option( - False, - "--factory", - is_flag=True, - help="Treat APP as an application factory.", - ), -) -> None: - """Generate project AsyncAPI schema.""" - if app_dir: # pragma: no branch - sys.path.insert(0, app_dir) - - _, app_obj = import_from_string(app, is_factory=is_factory) - - raw_schema = get_app_schema(app_obj) - - if yaml: - try: - schema = raw_schema.to_yaml() - except ImportError as e: # pragma: no cover - typer.echo(INSTALL_YAML, err=True) - raise typer.Exit(1) from e - - name = out or "asyncapi.yaml" - - with Path(name).open("w") as f: - f.write(schema) - - else: - schema = raw_schema.to_jsonable() - name = out or "asyncapi.json" - - with Path(name).open("w") as f: - json.dump(schema, f, indent=2) - - typer.echo(f"Your project AsyncAPI scheme was placed to `{name}`") - - -def _parse_and_serve( - app: str, - host: str = "localhost", - port: int = 8000, - is_factory: bool = False, -) -> None: - if ":" in app: - _, app_obj = import_from_string(app, is_factory=is_factory) - - raw_schema = get_app_schema(app_obj) - - else: - schema_filepath = Path.cwd() / app - - if schema_filepath.suffix == ".json": - data = schema_filepath.read_bytes() - - elif schema_filepath.suffix == ".yaml" or schema_filepath.suffix == ".yml": - try: - import yaml - except ImportError as e: # pragma: no cover - typer.echo(INSTALL_YAML, err=True) - raise typer.Exit(1) from e - - with schema_filepath.open("r") as f: - schema = yaml.safe_load(f) - - data = json_dumps(schema) - - else: - raise ValueError( - f"Unknown extension given - {app}; Please provide app in format [python_module:FastStream] or [asyncapi.yaml/.json] - path to your application or documentation" - ) - - raw_schema = model_parse(Schema, data) - - serve_app(raw_schema, host, port) diff --git a/faststream/cli/main.py b/faststream/cli/main.py deleted file mode 100644 index d7714d3cd6..0000000000 --- a/faststream/cli/main.py +++ /dev/null @@ -1,313 +0,0 @@ -import logging -import sys -import warnings -from contextlib import suppress -from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -import anyio -import typer -from click.exceptions import MissingParameter -from typer.core import TyperOption - -from faststream import FastStream -from faststream.__about__ import __version__ -from faststream._internal.application import Application -from faststream.asgi.app import AsgiFastStream -from faststream.cli.docs.app import docs_app -from faststream.cli.utils.imports import import_from_string -from faststream.cli.utils.logs import ( - LogFiles, - LogLevels, - get_log_level, - set_log_config, - set_log_level, -) -from faststream.cli.utils.parser import parse_cli_args -from faststream.exceptions import INSTALL_WATCHFILES, SetupError, ValidationError - -if TYPE_CHECKING: - from faststream.broker.core.usecase import BrokerUsecase - from faststream.types import AnyDict, SettingField - -cli = typer.Typer(pretty_exceptions_short=True) -cli.add_typer(docs_app, name="docs", help="AsyncAPI schema commands") - - -def version_callback(version: bool) -> None: - """Callback function for displaying version information.""" - if version: - import platform - - typer.echo( - f"Running FastStream {__version__} with {platform.python_implementation()} " - f"{platform.python_version()} on {platform.system()}" - ) - - raise typer.Exit() - - -@cli.callback() -def main( - version: Optional[bool] = typer.Option( - False, - "-v", - "--version", - callback=version_callback, - is_eager=True, - help="Show current platform, python and FastStream version.", - ), -) -> None: - """Generate, run and manage FastStream apps to greater development experience.""" - - -@cli.command( - context_settings={"allow_extra_args": True, "ignore_unknown_options": True} -) -def run( - ctx: typer.Context, - app: str = typer.Argument( - ..., - help="[python_module:FastStream] - path to your application.", - ), - workers: int = typer.Option( - 1, - show_default=False, - help="Run [workers] applications with process spawning.", - envvar="FASTSTREAM_WORKERS", - ), - log_level: LogLevels = typer.Option( - LogLevels.notset, - case_sensitive=False, - help="Set selected level for FastStream and brokers logger objects.", - envvar="FASTSTREAM_LOG_LEVEL", - ), - log_config: Optional[Path] = typer.Option( - None, - help=f"Set file to configure logging. Supported {[x.value for x in LogFiles]}", - ), - reload: bool = typer.Option( - False, - "--reload", - is_flag=True, - help="Restart app at directory files changes.", - ), - watch_extensions: List[str] = typer.Option( - (), - "--extension", - "--ext", - "--reload-extension", - "--reload-ext", - help="List of file extensions to watch by.", - ), - app_dir: str = typer.Option( - ".", - "--app-dir", - help=( - "Look for APP in the specified directory, by adding this to the PYTHONPATH." - " Defaults to the current working directory." - ), - envvar="FASTSTREAM_APP_DIR", - ), - is_factory: bool = typer.Option( - False, - "--factory", - is_flag=True, - help="Treat APP as an application factory.", - ), -) -> None: - """Run [MODULE:APP] FastStream application.""" - if watch_extensions and not reload: - typer.echo( - "Extra reload extensions has no effect without `--reload` flag." - "\nProbably, you forgot it?" - ) - - app, extra = parse_cli_args(app, *ctx.args) - casted_log_level = get_log_level(log_level) - - if app_dir: # pragma: no branch - sys.path.insert(0, app_dir) - - # Should be imported after sys.path changes - module_path, app_obj = import_from_string(app, is_factory=is_factory) - - args = (app, extra, is_factory, log_config, casted_log_level) - - if reload and workers > 1: - raise SetupError("You can't use reload option with multiprocessing") - - if reload: - try: - from faststream.cli.supervisors.watchfiles import WatchReloader - except ImportError: - warnings.warn(INSTALL_WATCHFILES, category=ImportWarning, stacklevel=1) - _run(*args) - - else: - if app_dir != ".": - reload_dirs = [str(module_path), app_dir] - else: - reload_dirs = [str(module_path)] - - WatchReloader( - target=_run, - args=args, - reload_dirs=reload_dirs, - extra_extensions=watch_extensions, - ).run() - - elif workers > 1: - if isinstance(app_obj, FastStream): - from faststream.cli.supervisors.multiprocess import Multiprocess - - Multiprocess( - target=_run, - args=(*args, logging.DEBUG), - workers=workers, - ).run() - elif isinstance(app_obj, AsgiFastStream): - from faststream.cli.supervisors.asgi_multiprocess import ASGIMultiprocess - - ASGIMultiprocess( - target=app, - args=args, # type: ignore[arg-type] - workers=workers, - ).run() - else: - raise typer.BadParameter( - f"Unexpected app type, expected FastStream or AsgiFastStream, got: {type(app_obj)}." - ) - - else: - _run_imported_app( - app_obj, - extra_options=extra, - log_level=casted_log_level, - log_config=log_config, - ) - - -def _run( - # NOTE: we should pass `str` due FastStream is not picklable - app: str, - extra_options: Dict[str, "SettingField"], - is_factory: bool, - log_config: Optional[Path], - log_level: int = logging.NOTSET, - app_level: int = logging.INFO, # option for reloader only -) -> None: - """Runs the specified application.""" - _, app_obj = import_from_string(app, is_factory=is_factory) - _run_imported_app( - app_obj, - extra_options=extra_options, - log_level=log_level, - app_level=app_level, - log_config=log_config, - ) - - -def _run_imported_app( - app_obj: "Application", - extra_options: Dict[str, "SettingField"], - log_config: Optional[Path], - log_level: int = logging.NOTSET, - app_level: int = logging.INFO, # option for reloader only -) -> None: - if not isinstance(app_obj, Application): - raise typer.BadParameter( - f'Imported object "{app_obj}" must be "Application" type.', - ) - - if log_level > 0: - set_log_level(log_level, app_obj) - - if log_config is not None: - set_log_config(log_config) - - if sys.platform not in ("win32", "cygwin", "cli"): # pragma: no cover - with suppress(ImportError): - import uvloop - - uvloop.install() - - try: - anyio.run( - app_obj.run, - app_level, - extra_options, - ) - - except ValidationError as e: - ex = MissingParameter( - message=( - "You registered extra options in your application " - "`lifespan/on_startup` hook, but does not set in CLI." - ), - param=TyperOption(param_decls=[f"--{x}" for x in e.fields]), - ) - - try: - from typer import rich_utils - - rich_utils.rich_format_error(ex) - except ImportError: - ex.show() - - sys.exit(1) - - -@cli.command( - context_settings={"allow_extra_args": True, "ignore_unknown_options": True} -) -def publish( - ctx: typer.Context, - app: str = typer.Argument(..., help="FastStream app instance, e.g., main:app."), - message: str = typer.Argument(..., help="Message to be published."), - rpc: bool = typer.Option(False, help="Enable RPC mode and system output."), - is_factory: bool = typer.Option( - False, - "--factory", - is_flag=True, - help="Treat APP as an application factory.", - ), -) -> None: - """Publish a message using the specified broker in a FastStream application. - - This command publishes a message to a broker configured in a FastStream app instance. - It supports various brokers and can handle extra arguments specific to each broker type. - These are parsed and passed to the broker's publish method. - """ - app, extra = parse_cli_args(app, *ctx.args) - extra["message"] = message - extra["rpc"] = rpc - - try: - if not app: - raise ValueError("App parameter is required.") - if not message: - raise ValueError("Message parameter is required.") - - _, app_obj = import_from_string(app, is_factory=is_factory) - - if not app_obj.broker: - raise ValueError("Broker instance not found in the app.") - - result = anyio.run(publish_message, app_obj.broker, extra) - - if rpc: - typer.echo(result) - - except Exception as e: - typer.echo(f"Publish error: {e}") - sys.exit(1) - - -async def publish_message(broker: "BrokerUsecase[Any, Any]", extra: "AnyDict") -> Any: - try: - async with broker: - return await broker.publish(**extra) - except Exception as e: - typer.echo(f"Error when broker was publishing: {e}") - sys.exit(1) diff --git a/faststream/cli/supervisors/asgi_multiprocess.py b/faststream/cli/supervisors/asgi_multiprocess.py deleted file mode 100644 index ecb85ed935..0000000000 --- a/faststream/cli/supervisors/asgi_multiprocess.py +++ /dev/null @@ -1,62 +0,0 @@ -import inspect -from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple - -from faststream._compat import HAS_UVICORN, UvicornMultiprocess, uvicorn -from faststream.asgi.app import cast_uvicorn_params -from faststream.exceptions import INSTALL_UVICORN - -if TYPE_CHECKING: - from faststream.types import SettingField - - -if HAS_UVICORN: - - class UvicornExtraConfig(uvicorn.Config): # type: ignore[misc] - def __init__( - self, - run_extra_options: Dict[str, "SettingField"], - *args: Any, - **kwargs: Any, - ) -> None: - super().__init__(*args, **kwargs) - self._run_extra_options = run_extra_options - - def load(self) -> None: - super().load() - self.loaded_app.app._run_extra_options = self._run_extra_options - - -class ASGIMultiprocess: - def __init__( - self, - target: str, - args: Tuple[str, Dict[str, str], bool, Optional[Path], int], - workers: int, - ) -> None: - _, run_extra_options, is_factory, _, log_level = args - self._target = target - self._run_extra_options = cast_uvicorn_params(run_extra_options or {}) - self._workers = workers - self._is_factory = is_factory - self._log_level = log_level - - def run(self) -> None: - if not HAS_UVICORN: - raise ImportError(INSTALL_UVICORN) - - config = UvicornExtraConfig( - app=self._target, - factory=self._is_factory, - log_level=self._log_level, - workers=self._workers, - **{ - key: v - for key, v in self._run_extra_options.items() - if key in set(inspect.signature(uvicorn.Config).parameters.keys()) - }, - run_extra_options=self._run_extra_options, - ) - server = uvicorn.Server(config) - sock = config.bind_socket() - UvicornMultiprocess(config, target=server.run, sockets=[sock]).run() diff --git a/faststream/cli/supervisors/basereload.py b/faststream/cli/supervisors/basereload.py deleted file mode 100644 index d0906cb56f..0000000000 --- a/faststream/cli/supervisors/basereload.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -import threading -from multiprocessing.context import SpawnProcess -from typing import TYPE_CHECKING, Any, Optional, Tuple - -from faststream.cli.supervisors.utils import get_subprocess, set_exit -from faststream.log import logger - -if TYPE_CHECKING: - from faststream.types import DecoratedCallable - - -class BaseReload: - """A base class for implementing a reloader process.""" - - _process: SpawnProcess - _target: "DecoratedCallable" - _args: Tuple[Any, ...] - - reload_delay: Optional[float] - should_exit: threading.Event - pid: int - reloader_name: str = "" - - def __init__( - self, - target: "DecoratedCallable", - args: Tuple[Any, ...], - reload_delay: Optional[float] = 0.5, - ) -> None: - self._target = target - self._args = args - - self.should_exit = threading.Event() - self.pid = os.getpid() - self.reload_delay = reload_delay - - set_exit(lambda *_: self.should_exit.set(), sync=True) - - def run(self) -> None: - self.startup() - while not self.should_exit.wait(self.reload_delay): - if self.should_restart(): # pragma: no branch - self.restart() - self.shutdown() - - def startup(self) -> None: - logger.info(f"Started reloader process [{self.pid}] using {self.reloader_name}") - self._process = self._start_process() - - def restart(self) -> None: - self._stop_process() - logger.info("Process successfully reloaded") - self._process = self._start_process() - - def shutdown(self) -> None: - self._stop_process() - logger.info(f"Stopping reloader process [{self.pid}]") - - def _stop_process(self) -> None: - self._process.terminate() - self._process.join() - - def _start_process(self) -> SpawnProcess: - process = get_subprocess(target=self._target, args=self._args) - process.start() - return process - - def should_restart(self) -> bool: - raise NotImplementedError("Reload strategies should override should_restart()") diff --git a/faststream/cli/supervisors/multiprocess.py b/faststream/cli/supervisors/multiprocess.py deleted file mode 100644 index a08fc5f273..0000000000 --- a/faststream/cli/supervisors/multiprocess.py +++ /dev/null @@ -1,69 +0,0 @@ -import signal -from typing import TYPE_CHECKING, Any, List, Tuple - -from faststream.cli.supervisors.basereload import BaseReload -from faststream.log import logger - -if TYPE_CHECKING: - from multiprocessing.context import SpawnProcess - - from faststream.types import DecoratedCallable - - -class Multiprocess(BaseReload): - """A class to represent a multiprocess.""" - - def __init__( - self, - target: "DecoratedCallable", - args: Tuple[Any, ...], - workers: int, - reload_delay: float = 0.5, - ) -> None: - super().__init__(target, args, reload_delay) - - self.workers = workers - self.processes: List[SpawnProcess] = [] - - def startup(self) -> None: - logger.info(f"Started parent process [{self.pid}]") - - for _ in range(self.workers): - process = self._start_process() - logger.info(f"Started child process [{process.pid}]") - self.processes.append(process) - - def shutdown(self) -> None: - for process in self.processes: - process.terminate() - logger.info(f"Stopping child process [{process.pid}]") - process.join() - - logger.info(f"Stopping parent process [{self.pid}]") - - def restart(self) -> None: - active_processes = [] - - for process in self.processes: - if process.is_alive(): - active_processes.append(process) - continue - - pid = process.pid - exitcode = process.exitcode - - log_msg = "Worker (pid:%s) exited with code %s." - if exitcode and abs(exitcode) == signal.SIGKILL: - log_msg += " Perhaps out of memory?" - logger.error(log_msg, pid, exitcode) - - process.kill() - - new_process = self._start_process() - logger.info(f"Started child process [{new_process.pid}]") - active_processes.append(new_process) - - self.processes = active_processes - - def should_restart(self) -> bool: - return not all(p.is_alive() for p in self.processes) diff --git a/faststream/cli/supervisors/utils.py b/faststream/cli/supervisors/utils.py deleted file mode 100644 index 0d39460bd8..0000000000 --- a/faststream/cli/supervisors/utils.py +++ /dev/null @@ -1,73 +0,0 @@ -import asyncio -import multiprocessing -import os -import signal -import sys -from contextlib import suppress -from typing import TYPE_CHECKING, Any, Callable, Optional - -if TYPE_CHECKING: - from multiprocessing.context import SpawnProcess - from types import FrameType - - from faststream.types import DecoratedCallableNone - -multiprocessing.allow_connection_pickling() -spawn = multiprocessing.get_context("spawn") - - -HANDLED_SIGNALS = ( - signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. - signal.SIGTERM, # Unix signal 15. Sent by `kill `. -) - - -def set_exit( - func: Callable[[int, Optional["FrameType"]], Any], - *, - sync: bool = False, -) -> None: - """Set exit handler for signals. - - Args: - func: A callable object that takes an integer and an optional frame type as arguments and returns any value. - sync: set sync or async signal callback. - """ - if not sync: - with suppress(NotImplementedError): - loop = asyncio.get_event_loop() - - for sig in HANDLED_SIGNALS: - loop.add_signal_handler(sig, func, sig, None) - - return - - # Windows or sync mode - for sig in HANDLED_SIGNALS: - signal.signal(sig, func) - - -def get_subprocess(target: "DecoratedCallableNone", args: Any) -> "SpawnProcess": - """Spawn a subprocess.""" - stdin_fileno: Optional[int] - try: - stdin_fileno = sys.stdin.fileno() - except OSError: - stdin_fileno = None - - return spawn.Process( - target=subprocess_started, - args=args, - kwargs={"t": target, "stdin_fileno": stdin_fileno}, - ) - - -def subprocess_started( - *args: Any, - t: "DecoratedCallableNone", - stdin_fileno: Optional[int], -) -> None: - """Start a subprocess.""" - if stdin_fileno is not None: # pragma: no cover - sys.stdin = os.fdopen(stdin_fileno) - t(*args) diff --git a/faststream/cli/utils/imports.py b/faststream/cli/utils/imports.py deleted file mode 100644 index a23fc1914e..0000000000 --- a/faststream/cli/utils/imports.py +++ /dev/null @@ -1,125 +0,0 @@ -import importlib -from importlib.util import module_from_spec, spec_from_file_location -from pathlib import Path -from typing import Tuple - -import typer - -from faststream._internal.application import Application -from faststream.exceptions import SetupError - - -def try_import_app(module: Path, app: str) -> "Application": - """Tries to import a FastStream app from a module.""" - try: - app_object = import_object(module, app) - - except FileNotFoundError as e: - typer.echo(e, err=True) - raise typer.BadParameter( - "Please, input module like [python_file:faststream_app_name] or [module:attribute]" - ) from e - - else: - return app_object # type: ignore - - -def import_object(module: Path, app: str) -> object: - """Import an object from a module.""" - spec = spec_from_file_location( - "mode", - f"{module}.py", - submodule_search_locations=[str(module.parent.absolute())], - ) - - if spec is None: # pragma: no cover - raise FileNotFoundError(module) - - mod = module_from_spec(spec) - loader = spec.loader - - if loader is None: # pragma: no cover - raise SetupError(f"{spec} has no loader") - - loader.exec_module(mod) - - try: - obj = getattr(mod, app) - except AttributeError as e: - raise FileNotFoundError(module) from e - - return obj - - -def get_app_path(app: str) -> Tuple[Path, str]: - """Get the application path.""" - if ":" not in app: - raise SetupError(f"`{app}` is not a FastStream") - - module, app_name = app.split(":", 2) - - mod_path = Path.cwd() - for i in module.split("."): - mod_path = mod_path / i - - return mod_path, app_name - - -def import_from_string( - import_str: str, - *, - is_factory: bool = False, -) -> Tuple[Path, "Application"]: - module_path, instance = _import_obj_or_factory(import_str) - - if is_factory: - if callable(instance): - instance = instance() - else: - raise typer.BadParameter(f'"{instance}" is not a factory') - - if callable(instance) and not is_factory and not isinstance(instance, Application): - raise typer.BadParameter("Please, use --factory option for callable object") - - return module_path, instance - - -def _import_obj_or_factory(import_str: str) -> Tuple[Path, "Application"]: - """Import FastStream application from module specified by a string.""" - if not isinstance(import_str, str): - raise typer.BadParameter("Given value is not of type string") - - module_str, _, attrs_str = import_str.partition(":") - if not module_str or not attrs_str: - raise typer.BadParameter( - f'Import string "{import_str}" must be in format ":"' - ) - - try: - module = importlib.import_module( # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import - module_str - ) - - except ModuleNotFoundError: - module_path, app_name = get_app_path(import_str) - instance = try_import_app(module_path, app_name) - - else: - attr = module - try: - for attr_str in attrs_str.split("."): - attr = getattr(attr, attr_str) - instance = attr # type: ignore[assignment] - - except AttributeError as e: - typer.echo(e, err=True) - raise typer.BadParameter( - f'Attribute "{attrs_str}" not found in module "{module_str}".' - ) from e - - if module.__file__: - module_path = Path(module.__file__).resolve().parent - else: - module_path = Path.cwd() - - return module_path, instance diff --git a/faststream/cli/utils/logs.py b/faststream/cli/utils/logs.py deleted file mode 100644 index 13eed22a4c..0000000000 --- a/faststream/cli/utils/logs.py +++ /dev/null @@ -1,150 +0,0 @@ -import json -import logging -import logging.config -from collections import defaultdict -from enum import Enum -from pathlib import Path -from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Optional, Union - -import typer - -from faststream.exceptions import INSTALL_TOML, INSTALL_YAML - -if TYPE_CHECKING: - from faststream._internal.application import Application - from faststream.types import LoggerProto - - -class LogLevels(str, Enum): - """A class to represent log levels. - - Attributes: - critical : critical log level - error : error log level - warning : warning log level - info : info log level - debug : debug log level - """ - - critical = "critical" - fatal = "fatal" - error = "error" - warning = "warning" - warn = "warn" - info = "info" - debug = "debug" - notset = "notset" - - -LOG_LEVELS: DefaultDict[str, int] = defaultdict( - lambda: logging.INFO, - **{ - "critical": logging.CRITICAL, - "fatal": logging.FATAL, - "error": logging.ERROR, - "warning": logging.WARNING, - "warn": logging.WARN, - "info": logging.INFO, - "debug": logging.DEBUG, - "notset": logging.NOTSET, - }, -) - - -class LogFiles(str, Enum): - """The class to represent supported log configuration files.""" - - json = ".json" - yaml = ".yaml" - yml = ".yml" - toml = ".toml" - - -def get_log_level(level: Union[LogLevels, str, int]) -> int: - """Get the log level. - - Args: - level: The log level to get. Can be an integer, a LogLevels enum value, or a string. - - Returns: - The log level as an integer. - - """ - if isinstance(level, int): - return level - - if isinstance(level, LogLevels): - return LOG_LEVELS[level.value] - - if isinstance(level, str): # pragma: no branch - return LOG_LEVELS[level.lower()] - - -def set_log_level(level: int, app: "Application") -> None: - """Sets the log level for an application.""" - if app.logger and getattr(app.logger, "setLevel", None): - app.logger.setLevel(level) # type: ignore[attr-defined] - - broker_logger: Optional[LoggerProto] = getattr(app.broker, "logger", None) - if broker_logger is not None and getattr(broker_logger, "setLevel", None): - broker_logger.setLevel(level) # type: ignore[attr-defined] - - -def _get_json_config(file: Path) -> Union[Dict[str, Any], Any]: - """Parse json config file to dict.""" - with file.open("r") as config_file: - return json.load(config_file) - - -def _get_yaml_config(file: Path) -> Union[Dict[str, Any], Any]: - """Parse yaml config file to dict.""" - try: - import yaml - except ImportError as e: - typer.echo(INSTALL_YAML, err=True) - raise typer.Exit(1) from e - - with file.open("r") as config_file: - return yaml.safe_load(config_file) - - -def _get_toml_config(file: Path) -> Union[Dict[str, Any], Any]: - """Parse toml config file to dict.""" - try: - import tomllib - except ImportError: - try: - import tomli as tomllib - except ImportError as e: - typer.echo(INSTALL_TOML, err=True) - raise typer.Exit(1) from e - - with file.open("rb") as config_file: - return tomllib.load(config_file) - - -def _get_log_config(file: Path) -> Union[Dict[str, Any], Any]: - """Read dict config from file.""" - if not file.exists(): - raise ValueError(f"File {file} specified to --log-config not found") - - file_format = file.suffix - - if file_format == LogFiles.json: - logging_config = _get_json_config(file) - elif file_format == LogFiles.yaml or file_format == LogFiles.yml: - logging_config = _get_yaml_config(file) - elif file_format == LogFiles.toml: - logging_config = _get_toml_config(file) - else: - raise ValueError( - f"Format {file_format} specified to --log-config file is not supported" - ) - - return logging_config - - -def set_log_config(file: Path) -> None: - """Set the logging config from file.""" - configuration = _get_log_config(file) - logging.config.dictConfig(configuration) diff --git a/faststream/cli/utils/parser.py b/faststream/cli/utils/parser.py deleted file mode 100644 index f36c54935c..0000000000 --- a/faststream/cli/utils/parser.py +++ /dev/null @@ -1,83 +0,0 @@ -import re -from functools import reduce -from typing import TYPE_CHECKING, Dict, List, Tuple - -if TYPE_CHECKING: - from faststream.types import SettingField - - -def is_bind_arg(arg: str) -> bool: - """Determine whether the received argument refers to --bind. - - bind arguments are like: 0.0.0.0:8000, [::]:8000, fd://2, /tmp/socket.sock - - """ - bind_regex = re.compile(r":\d+$|:/+\d|:/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+") - return bool(bind_regex.search(arg)) - - -def parse_cli_args(*args: str) -> Tuple[str, Dict[str, "SettingField"]]: - """Parses command line arguments.""" - extra_kwargs: Dict[str, SettingField] = {} - - k: str = "" - v: SettingField - - field_args: List[str] = [] - app = "" - for item in [ - *reduce( - lambda acc, x: acc + x.split("="), # type: ignore - args, - [], - ), - "-", - ]: - if ":" in item and not is_bind_arg(item): - app = item - - else: - if "-" in item: - if k: - k = k.strip().lstrip("-").replace("-", "_") - - if len(field_args) == 0: - v = not k.startswith("no_") - elif len(field_args) == 1: - v = field_args[0] - else: - v = field_args - - key = remove_prefix(k, "no_") - if (exists := extra_kwargs.get(key)) is not None: - v = [ - *(exists if isinstance(exists, list) else [exists]), - *(v if isinstance(v, list) else [v]), - ] - - extra_kwargs[key] = v - field_args = [] - - k = item - - else: - field_args.append(item) - - return app, extra_kwargs - - -def remove_prefix(text: str, prefix: str) -> str: - """Removes a prefix from a given text. - - Python 3.8 compatibility function - - Args: - text (str): The text from which the prefix will be removed. - prefix (str): The prefix to be removed from the text. - - Returns: - str: The text with the prefix removed. If the text does not start with the prefix, the original text is returned. - """ - if text.startswith(prefix): - return text[len(prefix) :] - return text diff --git a/faststream/confluent/__init__.py b/faststream/confluent/__init__.py index 29446b18e2..9d872e66d2 100644 --- a/faststream/confluent/__init__.py +++ b/faststream/confluent/__init__.py @@ -1,14 +1,16 @@ -try: - from faststream.testing.app import TestApp +from faststream._internal.testing.app import TestApp +try: from .annotations import KafkaMessage - from .broker import KafkaBroker + from .broker import KafkaBroker, KafkaPublisher, KafkaRoute, KafkaRouter from .response import KafkaResponse - from .router import KafkaPublisher, KafkaRoute, KafkaRouter from .schemas import TopicPartition from .testing import TestKafkaBroker except ImportError as e: + if "'confluent_kafka'" not in e.msg: + raise + from faststream.exceptions import INSTALL_FASTSTREAM_CONFLUENT raise ImportError(INSTALL_FASTSTREAM_CONFLUENT) from e diff --git a/faststream/confluent/annotations.py b/faststream/confluent/annotations.py index fec41b3817..e3c1f82af9 100644 --- a/faststream/confluent/annotations.py +++ b/faststream/confluent/annotations.py @@ -1,10 +1,11 @@ -from typing_extensions import Annotated +from typing import Annotated -from faststream.annotations import ContextRepo, Logger, NoCast +from faststream._internal.context import Context +from faststream.annotations import ContextRepo, Logger from faststream.confluent.broker import KafkaBroker as KB from faststream.confluent.message import KafkaMessage as KM from faststream.confluent.publisher.producer import AsyncConfluentFastProducer -from faststream.utils.context import Context +from faststream.params import NoCast __all__ = ( "ContextRepo", diff --git a/faststream/confluent/broker/__init__.py b/faststream/confluent/broker/__init__.py index a694e5c8e7..25ed570afd 100644 --- a/faststream/confluent/broker/__init__.py +++ b/faststream/confluent/broker/__init__.py @@ -1,3 +1,4 @@ -from faststream.confluent.broker.broker import KafkaBroker +from .broker import KafkaBroker +from .router import KafkaPublisher, KafkaRoute, KafkaRouter -__all__ = ("KafkaBroker",) +__all__ = ("KafkaBroker", "KafkaPublisher", "KafkaRoute", "KafkaRouter") diff --git a/faststream/confluent/broker/broker.py b/faststream/confluent/broker/broker.py index e183eea1c2..6fda92cbd3 100644 --- a/faststream/confluent/broker/broker.py +++ b/faststream/confluent/broker/broker.py @@ -1,342 +1,214 @@ import logging -import warnings -from functools import partial +from collections.abc import Callable, Iterable, Sequence from typing import ( TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, Literal, Optional, - Tuple, - Type, TypeVar, Union, ) import anyio -from typing_extensions import Annotated, Doc, deprecated, override +import confluent_kafka +from typing_extensions import override from faststream.__about__ import SERVICE_NAME -from faststream.broker.message import gen_cor_id -from faststream.confluent.broker.logging import KafkaLoggingBroker -from faststream.confluent.broker.registrator import KafkaRegistrator -from faststream.confluent.client import ( +from faststream._internal.broker import BrokerUsecase +from faststream._internal.constants import EMPTY +from faststream._internal.di import FastDependsConfig +from faststream.confluent.configs import KafkaBrokerConfig +from faststream.confluent.helpers import ( AsyncConfluentConsumer, - AsyncConfluentProducer, + ConfluentFastConfig, ) -from faststream.confluent.config import ConfluentFastConfig -from faststream.confluent.publisher.producer import AsyncConfluentFastProducer -from faststream.confluent.schemas.params import ConsumerConnectionParams -from faststream.confluent.security import parse_security -from faststream.exceptions import NOT_CONNECTED_YET -from faststream.types import EMPTY -from faststream.utils.data import filter_by_dict +from faststream.confluent.publisher.producer import AsyncConfluentFastProducerImpl +from faststream.confluent.response import KafkaPublishCommand +from faststream.message import gen_cor_id +from faststream.response.publish_type import PublishType +from faststream.specification.schema import BrokerSpec + +from .logging import make_kafka_logger_state +from .registrator import KafkaRegistrator if TYPE_CHECKING: + import asyncio from types import TracebackType from confluent_kafka import Message - from fast_depends.dependencies import Depends + from fast_depends.dependencies import Dependant + from fast_depends.library.serializer import SerializerProto - from faststream.asyncapi import schema as asyncapi - from faststream.broker.types import ( + from faststream._internal.basic_types import ( + LoggerProto, + SendableMessage, + ) + from faststream._internal.broker.abc_broker import Registrator + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, ) - from faststream.confluent.config import ConfluentConfig + from faststream.confluent.helpers.config import ConfluentConfig + from faststream.confluent.message import KafkaMessage from faststream.security import BaseSecurity - from faststream.types import ( - AnyDict, - AsyncFunc, - Decorator, - LoggerProto, - SendableMessage, - ) + from faststream.specification.schema.extra import Tag, TagDict Partition = TypeVar("Partition") class KafkaBroker( # type: ignore[misc] KafkaRegistrator, - KafkaLoggingBroker, + BrokerUsecase[ + Union[ + confluent_kafka.Message, + tuple[confluent_kafka.Message, ...], + ], + Callable[..., AsyncConfluentConsumer], + ], ): - url: List[str] - _producer: Optional[AsyncConfluentFastProducer] + url: list[str] def __init__( self, - bootstrap_servers: Annotated[ - Union[str, Iterable[str]], - Doc( - """ - A `host[:port]` string (or list of `host[:port]` strings) that the consumer should contact to bootstrap - initial cluster metadata. - - This does not have to be the full node list. - It just needs to have at least one broker that will respond to a - Metadata API Request. Default port is 9092. - """ - ), - ] = "localhost", + bootstrap_servers: str | Iterable[str] = "localhost", *, # both - request_timeout_ms: Annotated[ - int, - Doc("Client request timeout in milliseconds."), - ] = 40 * 1000, - retry_backoff_ms: Annotated[ - int, - Doc("Milliseconds to backoff when retrying on errors."), - ] = 100, - metadata_max_age_ms: Annotated[ - int, - Doc( - """ - The period of time in milliseconds after - which we force a refresh of metadata even if we haven't seen any - partition leadership changes to proactively discover any new - brokers or partitions. - """ - ), - ] = 5 * 60 * 1000, - connections_max_idle_ms: Annotated[ - int, - Doc( - """ - Close idle connections after the number - of milliseconds specified by this config. Specifying `None` will - disable idle checks. - """ - ), - ] = 9 * 60 * 1000, - client_id: Annotated[ - Optional[str], - Doc( - """ - A name for this client. This string is passed in - each request to servers and can be used to identify specific - server-side log entries that correspond to this client. Also - submitted to :class:`~.consumer.group_coordinator.GroupCoordinator` - for logging with respect to consumer group administration. - """ - ), - ] = SERVICE_NAME, - allow_auto_create_topics: Annotated[ - bool, - Doc( - """ - Allow automatic topic creation on the broker when subscribing to or assigning non-existent topics. - """ - ), - ] = True, - config: Annotated[ - Optional["ConfluentConfig"], - Doc( - """ - Extra configuration for the confluent-kafka-python - producer/consumer. See `confluent_kafka.Config `_. - """ - ), - ] = None, + request_timeout_ms: int = 40 * 1000, + retry_backoff_ms: int = 100, + metadata_max_age_ms: int = 5 * 60 * 1000, + connections_max_idle_ms: int = 9 * 60 * 1000, + client_id: str | None = SERVICE_NAME, + allow_auto_create_topics: bool = True, + config: Optional["ConfluentConfig"] = None, # publisher args - acks: Annotated[ - Literal[0, 1, -1, "all"], - Doc( - """ - One of ``0``, ``1``, ``all``. The number of acknowledgments - the producer requires the leader to have received before considering a - request complete. This controls the durability of records that are - sent. The following settings are common: - - * ``0``: Producer will not wait for any acknowledgment from the server - at all. The message will immediately be added to the socket - buffer and considered sent. No guarantee can be made that the - server has received the record in this case, and the retries - configuration will not take effect (as the client won't - generally know of any failures). The offset given back for each - record will always be set to -1. - * ``1``: The broker leader will write the record to its local log but - will respond without awaiting full acknowledgement from all - followers. In this case should the leader fail immediately - after acknowledging the record but before the followers have - replicated it then the record will be lost. - * ``all``: The broker leader will wait for the full set of in-sync - replicas to acknowledge the record. This guarantees that the - record will not be lost as long as at least one in-sync replica - remains alive. This is the strongest available guarantee. - - If unset, defaults to ``acks=1``. If `enable_idempotence` is - :data:`True` defaults to ``acks=all``. - """ - ), - ] = EMPTY, - compression_type: Annotated[ - Optional[Literal["gzip", "snappy", "lz4", "zstd"]], - Doc( - """ - The compression type for all data generated bythe producer. - Compression is of full batches of data, so the efficacy of batching - will also impact the compression ratio (more batching means better - compression). - """ - ), - ] = None, - partitioner: Annotated[ - Union[ - str, - Callable[ - [bytes, List[Partition], List[Partition]], - Partition, - ], - ], - Doc( - """ - Callable used to determine which partition - each message is assigned to. Called (after key serialization): - ``partitioner(key_bytes, all_partitions, available_partitions)``. - The default partitioner implementation hashes each non-None key - using the same murmur2 algorithm as the Java client so that - messages with the same key are assigned to the same partition. - When a key is :data:`None`, the message is delivered to a random partition - (filtered to partitions with available leaders only, if possible). - """ - ), - ] = "consistent_random", - max_request_size: Annotated[ - int, - Doc( - """ - The maximum size of a request. This is also - effectively a cap on the maximum record size. Note that the server - has its own cap on record size which may be different from this. - This setting will limit the number of record batches the producer - will send in a single request to avoid sending huge requests. - """ - ), - ] = 1024 * 1024, - linger_ms: Annotated[ - int, - Doc( - """ - The producer groups together any records that arrive - in between request transmissions into a single batched request. - Normally this occurs only under load when records arrive faster - than they can be sent out. However in some circumstances the client - may want to reduce the number of requests even under moderate load. - This setting accomplishes this by adding a small amount of - artificial delay; that is, if first request is processed faster, - than `linger_ms`, producer will wait ``linger_ms - process_time``. - """ - ), - ] = 0, - enable_idempotence: Annotated[ - bool, - Doc( - """ - When set to `True`, the producer will - ensure that exactly one copy of each message is written in the - stream. If `False`, producer retries due to broker failures, - etc., may write duplicates of the retried message in the stream. - Note that enabling idempotence acks to set to ``all``. If it is not - explicitly set by the user it will be chosen. - """ - ), - ] = False, - transactional_id: Optional[str] = None, + acks: Literal[0, 1, -1, "all"] = EMPTY, + compression_type: Literal["gzip", "snappy", "lz4", "zstd"] | None = None, + partitioner: str | Callable[[bytes, list[Partition], list[Partition]], Partition] = "consistent_random", + max_request_size: int = 1024 * 1024, + linger_ms: int = 0, + enable_idempotence: bool = False, + transactional_id: str | None = None, transaction_timeout_ms: int = 60 * 1000, # broker base args - graceful_timeout: Annotated[ - Optional[float], - Doc( - "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down." - ), - ] = 15.0, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Custom decoder object."), - ] = None, - parser: Annotated[ - Optional["CustomCallable"], - Doc("Custom parser object."), - ] = None, - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies to apply to all broker subscribers."), - ] = (), - middlewares: Annotated[ - Iterable[ - Union[ - "BrokerMiddleware[Message]", - "BrokerMiddleware[Tuple[Message, ...]]", - ] - ], - Doc("Middlewares to apply to all broker publishers/subscribers."), - ] = (), + graceful_timeout: float | None = 15.0, + decoder: Optional["CustomCallable"] = None, + parser: Optional["CustomCallable"] = None, + dependencies: Iterable["Dependant"] = (), + middlewares: Sequence["BrokerMiddleware[Message | tuple[Message, ...]]"] = (), + routers: Sequence["Registrator[Message]"] = (), # AsyncAPI args - security: Annotated[ - Optional["BaseSecurity"], - Doc( - "Security options to connect broker and generate AsyncAPI server security information." - ), - ] = None, - asyncapi_url: Annotated[ - Union[str, Iterable[str], None], - Doc("AsyncAPI hardcoded server addresses. Use `servers` if not specified."), - ] = None, - protocol: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol."), - ] = None, - protocol_version: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol version."), - ] = "auto", - description: Annotated[ - Optional[str], - Doc("AsyncAPI server description."), - ] = None, - tags: Annotated[ - Optional[Iterable[Union["asyncapi.Tag", "asyncapi.TagDict"]]], - Doc("AsyncAPI server tags."), - ] = None, + security: Optional["BaseSecurity"] = None, + specification_url: str | Iterable[str] | None = None, + protocol: str | None = None, + protocol_version: str | None = "auto", + description: str | None = None, + tags: Iterable[Union["Tag", "TagDict"]] = (), # logging args - logger: Annotated[ - Optional["LoggerProto"], - Doc("User specified logger to pass into Context and log service messages."), - ] = EMPTY, - log_level: Annotated[ - int, - Doc("Service messages log level."), - ] = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - Doc("Default logger log format."), - ] = EMPTY, + logger: Optional["LoggerProto"] = EMPTY, + log_level: int = logging.INFO, # FastDepends args - apply_types: Annotated[ - bool, - Doc("Whether to use FastDepends or not."), - ] = True, - validate: Annotated[ - bool, - Doc("Whether to cast types using Pydantic validation."), - ] = True, - _get_dependant: Annotated[ - Optional[Callable[..., Any]], - Doc("Custom library dependant generator callback."), - ] = None, - _call_decorators: Annotated[ - Iterable["Decorator"], - Doc("Any custom decorator to apply to wrapped functions."), - ] = (), + apply_types: bool = True, + serializer: Optional["SerializerProto"] = EMPTY, ) -> None: + """Initialize KafkaBroker. + + Args: + bootstrap_servers: A `host[:port]` string (or list of `host[:port]` strings) that the consumer should contact to bootstrap + initial cluster metadata. + + This does not have to be the full node list. + It just needs to have at least one broker that will respond to a + Metadata API Request. Default port is 9092. + request_timeout_ms: Client request timeout in milliseconds. + retry_backoff_ms: Milliseconds to backoff when retrying on errors. + metadata_max_age_ms: The period of time in milliseconds after + which we force a refresh of metadata even if we haven't seen any + partition leadership changes to proactively discover any new + brokers or partitions. + connections_max_idle_ms: Close idle connections after the number + of milliseconds specified by this config. Specifying `None` will + disable idle checks. + client_id: A name for this client. This string is passed in + each request to servers and can be used to identify specific + server-side log entries that correspond to this client. Also + submitted to :class:`~.consumer.group_coordinator.GroupCoordinator` + for logging with respect to consumer group administration. + allow_auto_create_topics: Allow automatic topic creation on the broker when subscribing to or assigning non-existent topics. + config: Extra configuration for the confluent-kafka-python + producer/consumer. See `confluent_kafka.Config `_. + acks: One of ``0``, ``1``, ``all``. The number of acknowledgments + the producer requires the leader to have received before considering a + request complete. This controls the durability of records that are + sent. The following settings are common: + + * ``0``: Producer will not wait for any acknowledgment from the server + at all. The message will immediately be added to the socket + buffer and considered sent. No guarantee can be made that the + server has received the record in this case, and the retries + configuration will not take effect (as the client won't + generally know of any failures). The offset given back for each + record will always be set to -1. + * ``1``: The broker leader will write the record to its local log but + will respond without awaiting full acknowledgement from all + followers. In this case should the leader fail immediately + after acknowledging the record but before the followers have + replicated it then the record will be lost. + * ``all``: The broker leader will wait for the full set of in-sync + replicas to acknowledge the record. This guarantees that the + record will not be lost as long as at least one in-sync replica + remains alive. This is the strongest available guarantee. + + If unset, defaults to ``acks=1``. If `enable_idempotence` is + :data:`True` defaults to ``acks=all``. + compression_type: The compression type for all data generated bythe producer. + Compression is of full batches of data, so the efficacy of batching + will also impact the compression ratio (more batching means better + compression). + partitioner: Callable used to determine which partition + each message is assigned to. Called (after key serialization): + ``partitioner(key_bytes, all_partitions, available_partitions)``. + The default partitioner implementation hashes each non-None key + using the same murmur2 algorithm as the Java client so that + messages with the same key are assigned to the same partition. + When a key is :data:`None`, the message is delivered to a random partition + (filtered to partitions with available leaders only, if possible). + max_request_size: The maximum size of a request. This is also + effectively a cap on the maximum record size. Note that the server + has its own cap on record size which may be different from this. + This setting will limit the number of record batches the producer + will send in a single request to avoid sending huge requests. + linger_ms: The producer groups together any records that arrive + in between request transmissions into a single batched request. + Normally this occurs only under load when records arrive faster + than they can be sent out. However in some circumstances the client + may want to reduce the number of requests even under moderate load. + This setting accomplishes this by adding a small amount of + artificial delay; that is, if first request is processed faster, + than `linger_ms`, producer will wait ``linger_ms - process_time``. + enable_idempotence: When set to `True`, the producer will + ensure that exactly one copy of each message is written in the + stream. If `False`, producer retries due to broker failures, + etc., may write duplicates of the retried message in the stream. + Note that enabling idempotence acks to set to ``all``. If it is not + explicitly set by the user it will be chosen. + transactional_id: Transactional ID for the producer. + transaction_timeout_ms: Transaction timeout in milliseconds. + graceful_timeout: Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down. + decoder: Custom decoder object. + parser: Custom parser object. + dependencies: Dependencies to apply to all broker subscribers. + middlewares: Middlewares to apply to all broker publishers/subscribers. + routers: Routers to apply to broker. + security: Security options to connect broker and generate AsyncAPI server security information. + specification_url: AsyncAPI hardcoded server addresses. Use `servers` if not specified. + protocol: AsyncAPI server protocol. + protocol_version: AsyncAPI server protocol version. + description: AsyncAPI server description. + tags: AsyncAPI server tags. + logger: User specified logger to pass into Context and log service messages. + log_level: Service messages log level. + apply_types: Whether to use FastDepends or not. + serializer: Serializer for FastDepends. + """ if protocol is None: if security is not None and security.use_ssl: protocol = "kafka-secure" @@ -349,17 +221,18 @@ def __init__( else list(bootstrap_servers) ) - if asyncapi_url is not None: - if isinstance(asyncapi_url, str): - asyncapi_url = [asyncapi_url] + if specification_url is not None: + if isinstance(specification_url, str): + specification_url = [specification_url] else: - asyncapi_url = list(asyncapi_url) + specification_url = list(specification_url) else: - asyncapi_url = servers + specification_url = servers - super().__init__( + connection_config = ConfluentFastConfig( + config=config, + security=security, bootstrap_servers=servers, - # both args client_id=client_id, request_timeout_ms=request_timeout_ms, retry_backoff_ms=retry_backoff_ms, @@ -375,217 +248,182 @@ def __init__( enable_idempotence=enable_idempotence, transactional_id=transactional_id, transaction_timeout_ms=transaction_timeout_ms, - # Basic args - graceful_timeout=graceful_timeout, - dependencies=dependencies, - decoder=decoder, - parser=parser, - middlewares=middlewares, - # AsyncAPI args - description=description, - asyncapi_url=asyncapi_url, - protocol=protocol, - protocol_version=protocol_version, - security=security, - tags=tags, - # Logging args - logger=logger, - log_level=log_level, - log_fmt=log_fmt, - # FastDepends args - _get_dependant=_get_dependant, - _call_decorators=_call_decorators, - apply_types=apply_types, - validate=validate, ) - self.client_id = client_id - self._producer = None - self.config = ConfluentFastConfig(config) - async def _close( - self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> None: - if self._producer is not None: # pragma: no branch - await self._producer.stop() - self._producer = None - - await super()._close(exc_type, exc_val, exc_tb) - - async def connect( - self, - bootstrap_servers: Annotated[ - Union[str, Iterable[str]], - Doc("Kafka addresses to connect."), - ] = EMPTY, - **kwargs: Any, - ) -> Callable[..., AsyncConfluentConsumer]: - if bootstrap_servers is not EMPTY or kwargs: - warnings.warn( - "`KafkaBroker().connect(...) options were " - "deprecated in **FastStream 0.5.40**. " - "Please, use `KafkaBroker(...)` instead. " - "All these options will be removed in **FastStream 0.6.0**.", - DeprecationWarning, - stacklevel=2, - ) - - if bootstrap_servers is not EMPTY: - kwargs["bootstrap_servers"] = bootstrap_servers - - return await super().connect(**kwargs) - - @override - async def _connect( # type: ignore[override] - self, - *, - client_id: str, - **kwargs: Any, - ) -> Callable[..., AsyncConfluentConsumer]: - security_params = parse_security(self.security) - kwargs.update(security_params) - - native_producer = AsyncConfluentProducer( - **kwargs, - client_id=client_id, - logger=self.logger, - config=self.config, + super().__init__( + routers=routers, + config=KafkaBrokerConfig( + connection_config=connection_config, + client_id=client_id, + producer=AsyncConfluentFastProducerImpl( + parser=parser, + decoder=decoder, + ), + # both args, + broker_decoder=decoder, + broker_parser=parser, + broker_middlewares=middlewares, + logger=make_kafka_logger_state( + logger=logger, + log_level=log_level, + ), + fd_config=FastDependsConfig( + use_fastdepends=apply_types, + serializer=serializer, + ), + # subscriber args + graceful_timeout=graceful_timeout, + broker_dependencies=dependencies, + extra_context={ + "broker": self, + }, + ), + specification=BrokerSpec( + description=description, + url=specification_url, + protocol=protocol, + protocol_version=protocol_version, + security=security, + tags=tags, + ), ) - self._producer = AsyncConfluentFastProducer( - producer=native_producer, - parser=self._parser, - decoder=self._decoder, - ) + @override + async def _connect(self) -> Callable[..., AsyncConfluentConsumer]: + await self.config.connect() + return self.config.builder - return partial( - AsyncConfluentConsumer, - **filter_by_dict(ConsumerConnectionParams, kwargs), - logger=self.logger, - config=self.config, - ) + async def close( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> None: + await super().close(exc_type, exc_val, exc_tb) + await self.config.disconnect() + self._connection = None async def start(self) -> None: + await self.connect() await super().start() - for handler in self._subscribers.values(): - self._log( - f"`{handler.call_name}` waiting for messages", - extra=handler.get_log_context(None), - ) - await handler.start() - - @property - def _subscriber_setup_extra(self) -> "AnyDict": - return { - **super()._subscriber_setup_extra, - "client_id": self.client_id, - "builder": self._connection, - } - @override async def publish( # type: ignore[override] self, message: "SendableMessage", topic: str, - key: Optional[bytes] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, *, + key: bytes | str | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, reply_to: str = "", no_confirm: bool = False, - # extra options to be compatible with test client - **kwargs: Any, - ) -> Optional[Any]: - correlation_id = correlation_id or gen_cor_id() - - return await super().publish( + ) -> "asyncio.Future": + """Publish message directly. + + This method allows you to publish message in not AsyncAPI-documented way. You can use it in another frameworks + applications or to publish messages from time to time. + + Please, use `@broker.publisher(...)` or `broker.publisher(...).publish(...)` instead in a regular way. + + Args: + message: Message body to send. + topic: Topic where the message will be published. + key: Message key for partitioning. + partition: Specific partition to publish to. + timestamp_ms: Message timestamp in milliseconds. + headers: Message headers to store metainformation. + correlation_id: Manual message **correlation_id** setter. **correlation_id** is a useful option to trace messages. + reply_to: Reply message topic name to send response. + no_confirm: Do not wait for Kafka publish confirmation. + + Returns: + asyncio.Future: Future object representing the publish operation. + """ + cmd = KafkaPublishCommand( message, - producer=self._producer, topic=topic, key=key, partition=partition, timestamp_ms=timestamp_ms, headers=headers, - correlation_id=correlation_id, reply_to=reply_to, no_confirm=no_confirm, - **kwargs, + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.PUBLISH, ) + return await super()._basic_publish(cmd, producer=self.config.producer) @override async def request( # type: ignore[override] self, message: "SendableMessage", topic: str, - key: Optional[bytes] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, + *, + key: bytes | str | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, timeout: float = 0.5, - ) -> Optional[Any]: - correlation_id = correlation_id or gen_cor_id() - - return await super().request( + ) -> "KafkaMessage": + cmd = KafkaPublishCommand( message, - producer=self._producer, topic=topic, key=key, partition=partition, timestamp_ms=timestamp_ms, headers=headers, - correlation_id=correlation_id, timeout=timeout, + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.REQUEST, + ) + + msg: KafkaMessage = await super()._basic_request( + cmd, producer=self.config.producer ) + return msg async def publish_batch( self, - *msgs: "SendableMessage", + *messages: "SendableMessage", topic: str, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, reply_to: str = "", - correlation_id: Optional[str] = None, + correlation_id: str | None = None, no_confirm: bool = False, ) -> None: - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - correlation_id = correlation_id or gen_cor_id() - - call: AsyncFunc = self._producer.publish_batch - for m in self._middlewares[::-1]: - call = partial(m(None).publish_scope, call) - - await call( - *msgs, + cmd = KafkaPublishCommand( + *messages, topic=topic, partition=partition, timestamp_ms=timestamp_ms, headers=headers, reply_to=reply_to, - correlation_id=correlation_id, no_confirm=no_confirm, + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.PUBLISH, ) + return await self._basic_publish_batch(cmd, producer=self.config.producer) + @override - async def ping(self, timeout: Optional[float]) -> bool: + async def ping(self, timeout: float | None) -> bool: sleep_time = (timeout or 10) / 10 with anyio.move_on_after(timeout) as cancel_scope: - if self._producer is None: + if not self.config.producer: return False while True: if cancel_scope.cancel_called: return False - if await self._producer._producer.ping(timeout=timeout): + if await self.config.producer.ping(timeout=timeout): return True await anyio.sleep(sleep_time) diff --git a/faststream/confluent/broker/logging.py b/faststream/confluent/broker/logging.py index d105a99560..541e94f6e5 100644 --- a/faststream/confluent/broker/logging.py +++ b/faststream/confluent/broker/logging.py @@ -1,80 +1,76 @@ import logging -from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Tuple, Union +from functools import partial +from typing import TYPE_CHECKING -from typing_extensions import Annotated, deprecated - -from faststream.broker.core.usecase import BrokerUsecase -from faststream.confluent.client import AsyncConfluentConsumer -from faststream.log.logging import get_broker_logger -from faststream.types import EMPTY +from faststream._internal.logger import ( + DefaultLoggerStorage, + make_logger_state, +) +from faststream._internal.logger.logging import get_broker_logger if TYPE_CHECKING: - import confluent_kafka + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.context import ContextRepo + - from faststream.types import LoggerProto +class KafkaParamsStorage(DefaultLoggerStorage): + def __init__(self) -> None: + super().__init__() + self._max_topic_len = 4 + self._max_group_len = 0 -class KafkaLoggingBroker( - BrokerUsecase[ - Union["confluent_kafka.Message", Tuple["confluent_kafka.Message", ...]], - Callable[..., AsyncConfluentConsumer], - ] -): - """A class that extends the LoggingMixin class and adds additional functionality for logging Kafka related information.""" + self.logger_log_level = logging.INFO - _max_topic_len: int - _max_group_len: int - __max_msg_id_ln: ClassVar[int] = 10 + def set_level(self, level: int) -> None: + self.logger_log_level = level - def __init__( - self, - *args: Any, - logger: Optional["LoggerProto"] = EMPTY, - log_level: int = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." + def register_subscriber(self, params: "AnyDict") -> None: + self._max_topic_len = max( + ( + self._max_topic_len, + len(params.get("topic", "")), ), - ] = EMPTY, - **kwargs: Any, - ) -> None: - """Initialize the class.""" - super().__init__( - *args, - logger=logger, - # TODO: generate unique logger names to not share between brokers - default_logger=get_broker_logger( + ) + self._max_group_len = max( + ( + self._max_group_len, + len(params.get("group_id", "")), + ), + ) + + def get_logger(self, *, context: "ContextRepo") -> "LoggerProto": + message_id_ln = 10 + + # TODO: generate unique logger names to not share between brokers + if not (lg := self._get_logger_ref()): + lg = get_broker_logger( name="confluent", default_context={ "topic": "", "group_id": "", }, - message_id_ln=self.__max_msg_id_ln, - ), - log_level=log_level, - log_fmt=log_fmt, - **kwargs, - ) - self._max_topic_len = 4 - self._max_group_len = 0 + message_id_ln=message_id_ln, + fmt="".join(( + "%(asctime)s %(levelname)-8s - ", + f"%(topic)-{self._max_topic_len}s | ", + ( + f"%(group_id)-{self._max_group_len}s | " + if self._max_group_len + else "" + ), + f"%(message_id)-{message_id_ln}s ", + "- %(message)s", + )), + context=context, + log_level=self.logger_log_level, + ) + self._logger_ref.add(lg) + + return lg - def get_fmt(self) -> str: - return ( - "%(asctime)s %(levelname)-8s - " - + f"%(topic)-{self._max_topic_len}s | " - + (f"%(group_id)-{self._max_group_len}s | " if self._max_group_len else "") - + f"%(message_id)-{self.__max_msg_id_ln}s " - + "- %(message)s" - ) - def _setup_log_context( - self, - *, - topic: str = "", - group_id: Optional[str] = None, - ) -> None: - """Set up log context.""" - self._max_topic_len = max((self._max_topic_len, len(topic))) - self._max_group_len = max((self._max_group_len, len(group_id or ""))) +make_kafka_logger_state = partial( + make_logger_state, + default_storage_cls=KafkaParamsStorage, +) diff --git a/faststream/confluent/broker/registrator.py b/faststream/confluent/broker/registrator.py index d0770a7b11..d3c560afae 100644 --- a/faststream/confluent/broker/registrator.py +++ b/faststream/confluent/broker/registrator.py @@ -1,1243 +1,359 @@ +from collections.abc import Iterable, Sequence from typing import ( TYPE_CHECKING, Any, - Dict, - Iterable, Literal, Optional, - Sequence, - Tuple, Union, cast, overload, ) -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import override -from faststream.broker.core.abc import ABCBroker -from faststream.broker.utils import default_filter -from faststream.confluent.subscriber.factory import create_publisher, create_subscriber +from faststream._internal.broker.abc_broker import Registrator +from faststream._internal.constants import EMPTY +from faststream.confluent.publisher.factory import create_publisher +from faststream.confluent.subscriber.factory import create_subscriber from faststream.exceptions import SetupError +from faststream.middlewares import AckPolicy if TYPE_CHECKING: from confluent_kafka import Message - from fast_depends.dependencies import Depends + from fast_depends.dependencies import Dependant - from faststream.broker.types import ( + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, - Filter, PublisherMiddleware, SubscriberMiddleware, ) + from faststream.confluent.configs import KafkaBrokerConfig from faststream.confluent.message import KafkaMessage - from faststream.confluent.publisher.asyncapi import ( - AsyncAPIBatchPublisher, - AsyncAPIDefaultPublisher, + from faststream.confluent.publisher.usecase import ( + BatchPublisher, + DefaultPublisher, ) from faststream.confluent.schemas import TopicPartition - from faststream.confluent.subscriber.asyncapi import ( - AsyncAPIBatchSubscriber, - AsyncAPIConcurrentDefaultSubscriber, - AsyncAPIDefaultSubscriber, + from faststream.confluent.subscriber.usecase import ( + BatchSubscriber, + ConcurrentDefaultSubscriber, + DefaultSubscriber, ) class KafkaRegistrator( - ABCBroker[ + Registrator[ Union[ "Message", - Tuple["Message", ...], + tuple["Message", ...], ] - ] + ], ): """Includable to KafkaBroker router.""" - _subscribers: Dict[ - int, - Union[ - "AsyncAPIBatchSubscriber", - "AsyncAPIDefaultSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", - ], + config: "KafkaBrokerConfig" + _subscribers: list[ # type: ignore[assignment] + Union["BatchSubscriber", "DefaultSubscriber", "ConcurrentDefaultSubscriber"], ] - _publishers: Dict[ # type: ignore[assignment] - int, Union["AsyncAPIBatchPublisher", "AsyncAPIDefaultPublisher"] + _publishers: list[ # type: ignore[assignment] + Union["BatchPublisher", "DefaultPublisher"] ] @overload # type: ignore[override] def subscriber( self, - *topics: Annotated[ - str, - Doc("Kafka topics to consume messages from."), - ], + *topics: str, partitions: Sequence["TopicPartition"] = (), polling_interval: float = 0.1, - group_id: Annotated[ - Optional[str], - Doc( - """ - Name of the consumer group to join for dynamic - partition assignment (if enabled), and to use for fetching and - committing offsets. If `None`, auto-partition assignment (via - group coordinator) and offset commits are disabled. - """ - ), - ] = None, - group_instance_id: Annotated[ - Optional[str], - Doc( - """ - A unique string that identifies the consumer instance. - If set, the consumer is treated as a static member of the group - and does not participate in consumer group management (e.g. - partition assignment, rebalances). This can be used to assign - partitions to specific consumers, rather than letting the group - assign partitions based on consumer metadata. - """ - ), - ] = None, - fetch_max_wait_ms: Annotated[ - int, - Doc( - """ - The maximum amount of time in milliseconds - the server will block before answering the fetch request if - there isn't sufficient data to immediately satisfy the - requirement given by `fetch_min_bytes`. - """ - ), - ] = 500, - fetch_max_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data the server should - return for a fetch request. This is not an absolute maximum, if - the first message in the first non-empty partition of the fetch - is larger than this value, the message will still be returned - to ensure that the consumer can make progress. NOTE: consumer - performs fetches to multiple brokers in parallel so memory - usage will depend on the number of brokers containing - partitions for the topic. - """ - ), - ] = 50 * 1024 * 1024, - fetch_min_bytes: Annotated[ - int, - Doc( - """ - Minimum amount of data the server should - return for a fetch request, otherwise wait up to - `fetch_max_wait_ms` for more data to accumulate. - """ - ), - ] = 1, - max_partition_fetch_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data - per-partition the server will return. The maximum total memory - used for a request ``= #partitions * max_partition_fetch_bytes``. - This size must be at least as large as the maximum message size - the server allows or else it is possible for the producer to - send messages larger than the consumer can fetch. If that - happens, the consumer can get stuck trying to fetch a large - message on a certain partition. - """ - ), - ] = 1 * 1024 * 1024, - auto_offset_reset: Annotated[ - Literal["latest", "earliest", "none"], - Doc( - """ - A policy for resetting offsets on `OffsetOutOfRangeError` errors: - - * `earliest` will move to the oldest available message - * `latest` will move to the most recent - * `none` will raise an exception so you can handle this case - """ - ), - ] = "latest", - auto_commit: Annotated[ - bool, - Doc( - """ - If `True` the consumer's offset will be - periodically committed in the background. - """ - ), - ] = True, - auto_commit_interval_ms: Annotated[ - int, - Doc( - """ - Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" - ), - ] = 5 * 1000, - check_crcs: Annotated[ - bool, - Doc( - """ - Automatically check the CRC32 of the records - consumed. This ensures no on-the-wire or on-disk corruption to - the messages occurred. This check adds some overhead, so it may - be disabled in cases seeking extreme performance. - """ - ), - ] = True, - partition_assignment_strategy: Annotated[ - Sequence[str], - Doc( - """ - List of objects to use to - distribute partition ownership amongst consumer instances when - group management is used. This preference is implicit in the order - of the strategies in the list. When assignment strategy changes: - to support a change to the assignment strategy, new versions must - enable support both for the old assignment strategy and the new - one. The coordinator will choose the old assignment strategy until - all members have been updated. Then it will choose the new - strategy. - """ - ), - ] = ("roundrobin",), - max_poll_interval_ms: Annotated[ - int, - Doc( - """ - Maximum allowed time between calls to - consume messages in batches. If this interval - is exceeded the consumer is considered failed and the group will - rebalance in order to reassign the partitions to another consumer - group member. If API methods block waiting for messages, that time - does not count against this timeout. - """ - ), - ] = 5 * 60 * 1000, - session_timeout_ms: Annotated[ - int, - Doc( - """ - Client group session and failure detection - timeout. The consumer sends periodic heartbeats - (`heartbeat.interval.ms`) to indicate its liveness to the broker. - If no hearts are received by the broker for a group member within - the session timeout, the broker will remove the consumer from the - group and trigger a rebalance. The allowed range is configured with - the **broker** configuration properties - `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ - ), - ] = 10 * 1000, - heartbeat_interval_ms: Annotated[ - int, - Doc( - """ - The expected time in milliseconds - between heartbeats to the consumer coordinator when using - Kafka's group management feature. Heartbeats are used to ensure - that the consumer's session stays active and to facilitate - rebalancing when new consumers join or leave the group. The - value must be set lower than `session_timeout_ms`, but typically - should be set no higher than 1/3 of that value. It can be - adjusted even lower to control the expected time for normal - rebalances. - """ - ), - ] = 3 * 1000, - isolation_level: Annotated[ - Literal["read_uncommitted", "read_committed"], - Doc( - """ - Controls how to read messages written - transactionally. - - * `read_committed`, batch consumer will only return - transactional messages which have been committed. - - * `read_uncommitted` (the default), batch consumer will - return all messages, even transactional messages which have been - aborted. - - Non-transactional messages will be returned unconditionally in - either mode. - - Messages will always be returned in offset order. Hence, in - `read_committed` mode, batch consumer will only return - messages up to the last stable offset (LSO), which is the one less - than the offset of the first open transaction. In particular any - messages appearing after messages belonging to ongoing transactions - will be withheld until the relevant transaction has been completed. - As a result, `read_committed` consumers will not be able to read up - to the high watermark when there are in flight transactions. - Further, when in `read_committed` the seek_to_end method will - return the LSO. See method docs below. - """ - ), + group_id: str | None = None, + group_instance_id: str | None = None, + fetch_max_wait_ms: int = 500, + fetch_max_bytes: int = 50 * 1024 * 1024, + fetch_min_bytes: int = 1, + max_partition_fetch_bytes: int = 1 * 1024 * 1024, + auto_offset_reset: Literal["latest", "earliest", "none"] = "latest", + auto_commit: bool = EMPTY, + auto_commit_interval_ms: int = 5 * 1000, + check_crcs: bool = True, + partition_assignment_strategy: Sequence[str] = ("roundrobin",), + max_poll_interval_ms: int = 5 * 60 * 1000, + session_timeout_ms: int = 10 * 1000, + heartbeat_interval_ms: int = 3 * 1000, + isolation_level: Literal[ + "read_uncommitted", "read_committed" ] = "read_uncommitted", - batch: Annotated[ - Literal[True], - Doc("Whether to consume messages in batches or not."), - ], - max_records: Annotated[ - Optional[int], - Doc("Number of messages to consume as one batch."), - ] = None, + batch: Literal[True], + max_records: int | None = None, # broker args - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **Message** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - middlewares: Annotated[ - Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), - deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - deprecated( - "Deprecated in **FastStream 0.5.40**." - "Please, manage acknowledgement policy manually." - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - max_workers: Annotated[ - int, - Doc("Number of workers to process messages concurrently."), - ] = 1, - ) -> "AsyncAPIBatchSubscriber": ... + dependencies: Iterable["Dependant"] = (), + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, + middlewares: Sequence["SubscriberMiddleware[KafkaMessage]"] = (), + no_ack: bool = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: bool = False, + # Specification args + title: str | None = None, + description: str | None = None, + include_in_schema: bool = True, + ) -> "SpecificationBatchSubscriber": ... @overload def subscriber( self, - *topics: Annotated[ - str, - Doc("Kafka topics to consume messages from."), - ], + *topics: str, partitions: Sequence["TopicPartition"] = (), polling_interval: float = 0.1, - group_id: Annotated[ - Optional[str], - Doc( - """ - Name of the consumer group to join for dynamic - partition assignment (if enabled), and to use for fetching and - committing offsets. If `None`, auto-partition assignment (via - group coordinator) and offset commits are disabled. - """ - ), - ] = None, - group_instance_id: Annotated[ - Optional[str], - Doc( - """ - A unique string that identifies the consumer instance. - If set, the consumer is treated as a static member of the group - and does not participate in consumer group management (e.g. - partition assignment, rebalances). This can be used to assign - partitions to specific consumers, rather than letting the group - assign partitions based on consumer metadata. - """ - ), - ] = None, - fetch_max_wait_ms: Annotated[ - int, - Doc( - """ - The maximum amount of time in milliseconds - the server will block before answering the fetch request if - there isn't sufficient data to immediately satisfy the - requirement given by `fetch_min_bytes`. - """ - ), - ] = 500, - fetch_max_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data the server should - return for a fetch request. This is not an absolute maximum, if - the first message in the first non-empty partition of the fetch - is larger than this value, the message will still be returned - to ensure that the consumer can make progress. NOTE: consumer - performs fetches to multiple brokers in parallel so memory - usage will depend on the number of brokers containing - partitions for the topic. - """ - ), - ] = 50 * 1024 * 1024, - fetch_min_bytes: Annotated[ - int, - Doc( - """ - Minimum amount of data the server should - return for a fetch request, otherwise wait up to - `fetch_max_wait_ms` for more data to accumulate. - """ - ), - ] = 1, - max_partition_fetch_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data - per-partition the server will return. The maximum total memory - used for a request ``= #partitions * max_partition_fetch_bytes``. - This size must be at least as large as the maximum message size - the server allows or else it is possible for the producer to - send messages larger than the consumer can fetch. If that - happens, the consumer can get stuck trying to fetch a large - message on a certain partition. - """ - ), - ] = 1 * 1024 * 1024, - auto_offset_reset: Annotated[ - Literal["latest", "earliest", "none"], - Doc( - """ - A policy for resetting offsets on `OffsetOutOfRangeError` errors: - - * `earliest` will move to the oldest available message - * `latest` will move to the most recent - * `none` will raise an exception so you can handle this case - """ - ), - ] = "latest", - auto_commit: Annotated[ - bool, - Doc( - """ - If `True` the consumer's offset will be - periodically committed in the background. - """ - ), - ] = True, - auto_commit_interval_ms: Annotated[ - int, - Doc( - """ - Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" - ), - ] = 5 * 1000, - check_crcs: Annotated[ - bool, - Doc( - """ - Automatically check the CRC32 of the records - consumed. This ensures no on-the-wire or on-disk corruption to - the messages occurred. This check adds some overhead, so it may - be disabled in cases seeking extreme performance. - """ - ), - ] = True, - partition_assignment_strategy: Annotated[ - Sequence[str], - Doc( - """ - List of objects to use to - distribute partition ownership amongst consumer instances when - group management is used. This preference is implicit in the order - of the strategies in the list. When assignment strategy changes: - to support a change to the assignment strategy, new versions must - enable support both for the old assignment strategy and the new - one. The coordinator will choose the old assignment strategy until - all members have been updated. Then it will choose the new - strategy. - """ - ), - ] = ("roundrobin",), - max_poll_interval_ms: Annotated[ - int, - Doc( - """ - Maximum allowed time between calls to - consume messages in batches. If this interval - is exceeded the consumer is considered failed and the group will - rebalance in order to reassign the partitions to another consumer - group member. If API methods block waiting for messages, that time - does not count against this timeout. - """ - ), - ] = 5 * 60 * 1000, - session_timeout_ms: Annotated[ - int, - Doc( - """ - Client group session and failure detection - timeout. The consumer sends periodic heartbeats - (`heartbeat.interval.ms`) to indicate its liveness to the broker. - If no hearts are received by the broker for a group member within - the session timeout, the broker will remove the consumer from the - group and trigger a rebalance. The allowed range is configured with - the **broker** configuration properties - `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ - ), - ] = 10 * 1000, - heartbeat_interval_ms: Annotated[ - int, - Doc( - """ - The expected time in milliseconds - between heartbeats to the consumer coordinator when using - Kafka's group management feature. Heartbeats are used to ensure - that the consumer's session stays active and to facilitate - rebalancing when new consumers join or leave the group. The - value must be set lower than `session_timeout_ms`, but typically - should be set no higher than 1/3 of that value. It can be - adjusted even lower to control the expected time for normal - rebalances. - """ - ), - ] = 3 * 1000, - isolation_level: Annotated[ - Literal["read_uncommitted", "read_committed"], - Doc( - """ - Controls how to read messages written - transactionally. - - * `read_committed`, batch consumer will only return - transactional messages which have been committed. - - * `read_uncommitted` (the default), batch consumer will - return all messages, even transactional messages which have been - aborted. - - Non-transactional messages will be returned unconditionally in - either mode. - - Messages will always be returned in offset order. Hence, in - `read_committed` mode, batch consumer will only return - messages up to the last stable offset (LSO), which is the one less - than the offset of the first open transaction. In particular any - messages appearing after messages belonging to ongoing transactions - will be withheld until the relevant transaction has been completed. - As a result, `read_committed` consumers will not be able to read up - to the high watermark when there are in flight transactions. - Further, when in `read_committed` the seek_to_end method will - return the LSO. See method docs below. - """ - ), + group_id: str | None = None, + group_instance_id: str | None = None, + fetch_max_wait_ms: int = 500, + fetch_max_bytes: int = 50 * 1024 * 1024, + fetch_min_bytes: int = 1, + max_partition_fetch_bytes: int = 1 * 1024 * 1024, + auto_offset_reset: Literal["latest", "earliest", "none"] = "latest", + auto_commit: bool = EMPTY, + auto_commit_interval_ms: int = 5 * 1000, + check_crcs: bool = True, + partition_assignment_strategy: Sequence[str] = ("roundrobin",), + max_poll_interval_ms: int = 5 * 60 * 1000, + session_timeout_ms: int = 10 * 1000, + heartbeat_interval_ms: int = 3 * 1000, + isolation_level: Literal[ + "read_uncommitted", "read_committed" ] = "read_uncommitted", - batch: Annotated[ - Literal[False], - Doc("Whether to consume messages in batches or not."), - ] = False, - max_records: Annotated[ - Optional[int], - Doc("Number of messages to consume as one batch."), - ] = None, + batch: Literal[False] = False, + max_records: int | None = None, # broker args - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **Message** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - middlewares: Annotated[ - Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), - deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - deprecated( - "Deprecated in **FastStream 0.5.40**." - "Please, manage acknowledgement policy manually." - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - max_workers: Annotated[ - int, - Doc("Number of workers to process messages concurrently."), - ] = 1, - ) -> "AsyncAPIDefaultSubscriber": ... + dependencies: Iterable["Dependant"] = (), + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, + middlewares: Sequence["SubscriberMiddleware[KafkaMessage]"] = (), + no_ack: bool = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: bool = False, + # Specification args + title: str | None = None, + description: str | None = None, + include_in_schema: bool = True, + ) -> Union[ + "DefaultSubscriber", + "ConcurrentDefaultSubscriber", + ]: ... @overload def subscriber( self, - *topics: Annotated[ - str, - Doc("Kafka topics to consume messages from."), - ], + *topics: str, partitions: Sequence["TopicPartition"] = (), polling_interval: float = 0.1, - group_id: Annotated[ - Optional[str], - Doc( - """ - Name of the consumer group to join for dynamic - partition assignment (if enabled), and to use for fetching and - committing offsets. If `None`, auto-partition assignment (via - group coordinator) and offset commits are disabled. - """ - ), - ] = None, - group_instance_id: Annotated[ - Optional[str], - Doc( - """ - A unique string that identifies the consumer instance. - If set, the consumer is treated as a static member of the group - and does not participate in consumer group management (e.g. - partition assignment, rebalances). This can be used to assign - partitions to specific consumers, rather than letting the group - assign partitions based on consumer metadata. - """ - ), - ] = None, - fetch_max_wait_ms: Annotated[ - int, - Doc( - """ - The maximum amount of time in milliseconds - the server will block before answering the fetch request if - there isn't sufficient data to immediately satisfy the - requirement given by `fetch_min_bytes`. - """ - ), - ] = 500, - fetch_max_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data the server should - return for a fetch request. This is not an absolute maximum, if - the first message in the first non-empty partition of the fetch - is larger than this value, the message will still be returned - to ensure that the consumer can make progress. NOTE: consumer - performs fetches to multiple brokers in parallel so memory - usage will depend on the number of brokers containing - partitions for the topic. - """ - ), - ] = 50 * 1024 * 1024, - fetch_min_bytes: Annotated[ - int, - Doc( - """ - Minimum amount of data the server should - return for a fetch request, otherwise wait up to - `fetch_max_wait_ms` for more data to accumulate. - """ - ), - ] = 1, - max_partition_fetch_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data - per-partition the server will return. The maximum total memory - used for a request ``= #partitions * max_partition_fetch_bytes``. - This size must be at least as large as the maximum message size - the server allows or else it is possible for the producer to - send messages larger than the consumer can fetch. If that - happens, the consumer can get stuck trying to fetch a large - message on a certain partition. - """ - ), - ] = 1 * 1024 * 1024, - auto_offset_reset: Annotated[ - Literal["latest", "earliest", "none"], - Doc( - """ - A policy for resetting offsets on `OffsetOutOfRangeError` errors: - - * `earliest` will move to the oldest available message - * `latest` will move to the most recent - * `none` will raise an exception so you can handle this case - """ - ), - ] = "latest", - auto_commit: Annotated[ - bool, - Doc( - """ - If `True` the consumer's offset will be - periodically committed in the background. - """ - ), - ] = True, - auto_commit_interval_ms: Annotated[ - int, - Doc( - """ - Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" - ), - ] = 5 * 1000, - check_crcs: Annotated[ - bool, - Doc( - """ - Automatically check the CRC32 of the records - consumed. This ensures no on-the-wire or on-disk corruption to - the messages occurred. This check adds some overhead, so it may - be disabled in cases seeking extreme performance. - """ - ), - ] = True, - partition_assignment_strategy: Annotated[ - Sequence[str], - Doc( - """ - List of objects to use to - distribute partition ownership amongst consumer instances when - group management is used. This preference is implicit in the order - of the strategies in the list. When assignment strategy changes: - to support a change to the assignment strategy, new versions must - enable support both for the old assignment strategy and the new - one. The coordinator will choose the old assignment strategy until - all members have been updated. Then it will choose the new - strategy. - """ - ), - ] = ("roundrobin",), - max_poll_interval_ms: Annotated[ - int, - Doc( - """ - Maximum allowed time between calls to - consume messages in batches. If this interval - is exceeded the consumer is considered failed and the group will - rebalance in order to reassign the partitions to another consumer - group member. If API methods block waiting for messages, that time - does not count against this timeout. - """ - ), - ] = 5 * 60 * 1000, - session_timeout_ms: Annotated[ - int, - Doc( - """ - Client group session and failure detection - timeout. The consumer sends periodic heartbeats - (`heartbeat.interval.ms`) to indicate its liveness to the broker. - If no hearts are received by the broker for a group member within - the session timeout, the broker will remove the consumer from the - group and trigger a rebalance. The allowed range is configured with - the **broker** configuration properties - `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ - ), - ] = 10 * 1000, - heartbeat_interval_ms: Annotated[ - int, - Doc( - """ - The expected time in milliseconds - between heartbeats to the consumer coordinator when using - Kafka's group management feature. Heartbeats are used to ensure - that the consumer's session stays active and to facilitate - rebalancing when new consumers join or leave the group. The - value must be set lower than `session_timeout_ms`, but typically - should be set no higher than 1/3 of that value. It can be - adjusted even lower to control the expected time for normal - rebalances. - """ - ), - ] = 3 * 1000, - isolation_level: Annotated[ - Literal["read_uncommitted", "read_committed"], - Doc( - """ - Controls how to read messages written - transactionally. - - * `read_committed`, batch consumer will only return - transactional messages which have been committed. - - * `read_uncommitted` (the default), batch consumer will - return all messages, even transactional messages which have been - aborted. - - Non-transactional messages will be returned unconditionally in - either mode. - - Messages will always be returned in offset order. Hence, in - `read_committed` mode, batch consumer will only return - messages up to the last stable offset (LSO), which is the one less - than the offset of the first open transaction. In particular any - messages appearing after messages belonging to ongoing transactions - will be withheld until the relevant transaction has been completed. - As a result, `read_committed` consumers will not be able to read up - to the high watermark when there are in flight transactions. - Further, when in `read_committed` the seek_to_end method will - return the LSO. See method docs below. - """ - ), + group_id: str | None = None, + group_instance_id: str | None = None, + fetch_max_wait_ms: int = 500, + fetch_max_bytes: int = 50 * 1024 * 1024, + fetch_min_bytes: int = 1, + max_partition_fetch_bytes: int = 1 * 1024 * 1024, + auto_offset_reset: Literal["latest", "earliest", "none"] = "latest", + auto_commit: bool = EMPTY, + auto_commit_interval_ms: int = 5 * 1000, + check_crcs: bool = True, + partition_assignment_strategy: Sequence[str] = ("roundrobin",), + max_poll_interval_ms: int = 5 * 60 * 1000, + session_timeout_ms: int = 10 * 1000, + heartbeat_interval_ms: int = 3 * 1000, + isolation_level: Literal[ + "read_uncommitted", "read_committed" ] = "read_uncommitted", - batch: Annotated[ - bool, - Doc("Whether to consume messages in batches or not."), - ] = False, - max_records: Annotated[ - Optional[int], - Doc("Number of messages to consume as one batch."), - ] = None, + batch: bool = False, + max_records: int | None = None, # broker args - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **Message** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - middlewares: Annotated[ - Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), - deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - deprecated( - "Deprecated in **FastStream 0.5.40**." - "Please, manage acknowledgement policy manually." - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - max_workers: Annotated[ - int, - Doc("Number of workers to process messages concurrently."), - ] = 1, + dependencies: Iterable["Dependant"] = (), + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, + middlewares: Sequence["SubscriberMiddleware[KafkaMessage]"] = (), + no_ack: bool = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: bool = False, + # Specification args + title: str | None = None, + description: str | None = None, + include_in_schema: bool = True, + max_workers: int = 1, ) -> Union[ - "AsyncAPIDefaultSubscriber", - "AsyncAPIBatchSubscriber", + "DefaultSubscriber", + "BatchSubscriber", + "ConcurrentDefaultSubscriber", ]: ... @override def subscriber( self, - *topics: Annotated[ - str, - Doc("Kafka topics to consume messages from."), - ], + *topics: str, partitions: Sequence["TopicPartition"] = (), polling_interval: float = 0.1, - group_id: Annotated[ - Optional[str], - Doc( - """ - Name of the consumer group to join for dynamic - partition assignment (if enabled), and to use for fetching and - committing offsets. If `None`, auto-partition assignment (via - group coordinator) and offset commits are disabled. - """ - ), - ] = None, - group_instance_id: Annotated[ - Optional[str], - Doc( - """ - A unique string that identifies the consumer instance. - If set, the consumer is treated as a static member of the group - and does not participate in consumer group management (e.g. - partition assignment, rebalances). This can be used to assign - partitions to specific consumers, rather than letting the group - assign partitions based on consumer metadata. - """ - ), - ] = None, - fetch_max_wait_ms: Annotated[ - int, - Doc( - """ - The maximum amount of time in milliseconds - the server will block before answering the fetch request if - there isn't sufficient data to immediately satisfy the - requirement given by `fetch_min_bytes`. - """ - ), - ] = 500, - fetch_max_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data the server should - return for a fetch request. This is not an absolute maximum, if - the first message in the first non-empty partition of the fetch - is larger than this value, the message will still be returned - to ensure that the consumer can make progress. NOTE: consumer - performs fetches to multiple brokers in parallel so memory - usage will depend on the number of brokers containing - partitions for the topic. - """ - ), - ] = 50 * 1024 * 1024, - fetch_min_bytes: Annotated[ - int, - Doc( - """ - Minimum amount of data the server should - return for a fetch request, otherwise wait up to - `fetch_max_wait_ms` for more data to accumulate. - """ - ), - ] = 1, - max_partition_fetch_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data - per-partition the server will return. The maximum total memory - used for a request ``= #partitions * max_partition_fetch_bytes``. - This size must be at least as large as the maximum message size - the server allows or else it is possible for the producer to - send messages larger than the consumer can fetch. If that - happens, the consumer can get stuck trying to fetch a large - message on a certain partition. - """ - ), - ] = 1 * 1024 * 1024, - auto_offset_reset: Annotated[ - Literal["latest", "earliest", "none"], - Doc( - """ - A policy for resetting offsets on `OffsetOutOfRangeError` errors: - - * `earliest` will move to the oldest available message - * `latest` will move to the most recent - * `none` will raise an exception so you can handle this case - """ - ), - ] = "latest", - auto_commit: Annotated[ - bool, - Doc( - """ - If `True` the consumer's offset will be - periodically committed in the background. - """ - ), - ] = True, - auto_commit_interval_ms: Annotated[ - int, - Doc( - """ - Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" - ), - ] = 5 * 1000, - check_crcs: Annotated[ - bool, - Doc( - """ - Automatically check the CRC32 of the records - consumed. This ensures no on-the-wire or on-disk corruption to - the messages occurred. This check adds some overhead, so it may - be disabled in cases seeking extreme performance. - """ - ), - ] = True, - partition_assignment_strategy: Annotated[ - Sequence[str], - Doc( - """ - List of objects to use to - distribute partition ownership amongst consumer instances when - group management is used. This preference is implicit in the order - of the strategies in the list. When assignment strategy changes: - to support a change to the assignment strategy, new versions must - enable support both for the old assignment strategy and the new - one. The coordinator will choose the old assignment strategy until - all members have been updated. Then it will choose the new - strategy. - """ - ), - ] = ("roundrobin",), - max_poll_interval_ms: Annotated[ - int, - Doc( - """ - Maximum allowed time between calls to - consume messages in batches. If this interval - is exceeded the consumer is considered failed and the group will - rebalance in order to reassign the partitions to another consumer - group member. If API methods block waiting for messages, that time - does not count against this timeout. - """ - ), - ] = 5 * 60 * 1000, - session_timeout_ms: Annotated[ - int, - Doc( - """ - Client group session and failure detection - timeout. The consumer sends periodic heartbeats - (`heartbeat.interval.ms`) to indicate its liveness to the broker. - If no hearts are received by the broker for a group member within - the session timeout, the broker will remove the consumer from the - group and trigger a rebalance. The allowed range is configured with - the **broker** configuration properties - `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ - ), - ] = 10 * 1000, - heartbeat_interval_ms: Annotated[ - int, - Doc( - """ - The expected time in milliseconds - between heartbeats to the consumer coordinator when using - Kafka's group management feature. Heartbeats are used to ensure - that the consumer's session stays active and to facilitate - rebalancing when new consumers join or leave the group. The - value must be set lower than `session_timeout_ms`, but typically - should be set no higher than 1/3 of that value. It can be - adjusted even lower to control the expected time for normal - rebalances. - """ - ), - ] = 3 * 1000, - isolation_level: Annotated[ - Literal["read_uncommitted", "read_committed"], - Doc( - """ - Controls how to read messages written - transactionally. - - * `read_committed`, batch consumer will only return - transactional messages which have been committed. - - * `read_uncommitted` (the default), batch consumer will - return all messages, even transactional messages which have been - aborted. - - Non-transactional messages will be returned unconditionally in - either mode. - - Messages will always be returned in offset order. Hence, in - `read_committed` mode, batch consumer will only return - messages up to the last stable offset (LSO), which is the one less - than the offset of the first open transaction. In particular any - messages appearing after messages belonging to ongoing transactions - will be withheld until the relevant transaction has been completed. - As a result, `read_committed` consumers will not be able to read up - to the high watermark when there are in flight transactions. - Further, when in `read_committed` the seek_to_end method will - return the LSO. See method docs below. - """ - ), + group_id: str | None = None, + group_instance_id: str | None = None, + fetch_max_wait_ms: int = 500, + fetch_max_bytes: int = 50 * 1024 * 1024, + fetch_min_bytes: int = 1, + max_partition_fetch_bytes: int = 1 * 1024 * 1024, + auto_offset_reset: Literal["latest", "earliest", "none"] = "latest", + auto_commit: bool = EMPTY, + auto_commit_interval_ms: int = 5 * 1000, + check_crcs: bool = True, + partition_assignment_strategy: Sequence[str] = ("roundrobin",), + max_poll_interval_ms: int = 5 * 60 * 1000, + session_timeout_ms: int = 10 * 1000, + heartbeat_interval_ms: int = 3 * 1000, + isolation_level: Literal[ + "read_uncommitted", "read_committed" ] = "read_uncommitted", - batch: Annotated[ - bool, - Doc("Whether to consume messages in batches or not."), - ] = False, - max_records: Annotated[ - Optional[int], - Doc("Number of messages to consume as one batch."), - ] = None, + batch: bool = False, + max_records: int | None = None, # broker args - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **Message** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - middlewares: Annotated[ - Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), - deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - deprecated( - "Deprecated in **FastStream 0.5.40**." - "Please, manage acknowledgement policy manually." - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - max_workers: Annotated[ - int, - Doc("Number of workers to process messages concurrently."), - ] = 1, + dependencies: Iterable["Dependant"] = (), + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, + middlewares: Sequence["SubscriberMiddleware[KafkaMessage]"] = (), + no_ack: bool = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: bool = False, + # Specification args + title: str | None = None, + description: str | None = None, + include_in_schema: bool = True, + max_workers: int = 1, ) -> Union[ - "AsyncAPIDefaultSubscriber", - "AsyncAPIBatchSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", + "DefaultSubscriber", + "BatchSubscriber", + "ConcurrentDefaultSubscriber", ]: - if not auto_commit and not group_id: - raise SetupError("You should install `group_id` with manual commit mode") - + """Create a subscriber for Kafka topics. + + Args: + *topics: Kafka topics to consume messages from. + partitions: Sequence of topic partitions. + polling_interval: Polling interval in seconds. + group_id: Name of the consumer group to join for dynamic + partition assignment (if enabled), and to use for fetching and + committing offsets. If `None`, auto-partition assignment (via + group coordinator) and offset commits are disabled. + group_instance_id: A unique string that identifies the consumer instance. + If set, the consumer is treated as a static member of the group + and does not participate in consumer group management (e.g. + partition assignment, rebalances). This can be used to assign + partitions to specific consumers, rather than letting the group + assign partitions based on consumer metadata. + fetch_max_wait_ms: The maximum amount of time in milliseconds + the server will block before answering the fetch request if + there isn't sufficient data to immediately satisfy the + requirement given by `fetch_min_bytes`. + fetch_max_bytes: The maximum amount of data the server should + return for a fetch request. This is not an absolute maximum, if + the first message in the first non-empty partition of the fetch + is larger than this value, the message will still be returned + to ensure that the consumer can make progress. NOTE: consumer + performs fetches to multiple brokers in parallel so memory + usage will depend on the number of brokers containing + partitions for the topic. + fetch_min_bytes: Minimum amount of data the server should + return for a fetch request, otherwise wait up to + `fetch_max_wait_ms` for more data to accumulate. + max_partition_fetch_bytes: The maximum amount of data + per-partition the server will return. The maximum total memory + used for a request ``= #partitions * max_partition_fetch_bytes``. + This size must be at least as large as the maximum message size + the server allows or else it is possible for the producer to + send messages larger than the consumer can fetch. If that + happens, the consumer can get stuck trying to fetch a large + message on a certain partition. + auto_offset_reset: A policy for resetting offsets on `OffsetOutOfRangeError` errors: + + * `earliest` will move to the oldest available message + * `latest` will move to the most recent + * `none` will raise an exception so you can handle this case + auto_commit: If `True` the consumer's offset will be + periodically committed in the background. + auto_commit_interval_ms: Milliseconds between automatic + offset commits, if `auto_commit` is `True`. + check_crcs: Automatically check the CRC32 of the records + consumed. This ensures no on-the-wire or on-disk corruption to + the messages occurred. This check adds some overhead, so it may + be disabled in cases seeking extreme performance. + partition_assignment_strategy: List of objects to use to + distribute partition ownership amongst consumer instances when + group management is used. This preference is implicit in the order + of the strategies in the list. When assignment strategy changes: + to support a change to the assignment strategy, new versions must + enable support both for the old assignment strategy and the new + one. The coordinator will choose the old assignment strategy until + all members have been updated. Then it will choose the new + strategy. + max_poll_interval_ms: Maximum allowed time between calls to + consume messages in batches. If this interval + is exceeded the consumer is considered failed and the group will + rebalance in order to reassign the partitions to another consumer + group member. If API methods block waiting for messages, that time + does not count against this timeout. + session_timeout_ms: Client group session and failure detection + timeout. The consumer sends periodic heartbeats + (`heartbeat.interval.ms`) to indicate its liveness to the broker. + If no hearts are received by the broker for a group member within + the session timeout, the broker will remove the consumer from the + group and trigger a rebalance. The allowed range is configured with + the **broker** configuration properties + `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. + heartbeat_interval_ms: The expected time in milliseconds + between heartbeats to the consumer coordinator when using + Kafka's group management feature. Heartbeats are used to ensure + that the consumer's session stays active and to facilitate + rebalancing when new consumers join or leave the group. The + value must be set lower than `session_timeout_ms`, but typically + should be set no higher than 1/3 of that value. It can be + adjusted even lower to control the expected time for normal + rebalances. + isolation_level: Controls how to read messages written + transactionally. + + * `read_committed`, batch consumer will only return + transactional messages which have been committed. + + * `read_uncommitted` (the default), batch consumer will + return all messages, even transactional messages which have been + aborted. + + Non-transactional messages will be returned unconditionally in + either mode. + + Messages will always be returned in offset order. Hence, in + `read_committed` mode, batch consumer will only return + messages up to the last stable offset (LSO), which is the one less + than the offset of the first open transaction. In particular any + messages appearing after messages belonging to ongoing transactions + will be withheld until the relevant transaction has been completed. + As a result, `read_committed` consumers will not be able to read up + to the high watermark when there are in flight transactions. + Further, when in `read_committed` the seek_to_end method will + return the LSO. See method docs below. + batch: Whether to consume messages in batches or not. + max_records: Number of messages to consume as one batch. + dependencies: Dependencies list (`[Dependant(),]`) to apply to the subscriber. + parser: Parser to map original **Message** object to FastStream one. + decoder: Function to decode FastStream msg bytes body to python objects. + middlewares: Subscriber middlewares to wrap incoming message processing. + no_ack: Whether to disable **FastStream** auto acknowledgement logic or not. + ack_policy: Acknowledgement policy for the subscriber. + no_reply: Whether to disable **FastStream** RPC and Reply To auto responses or not. + title: Specification subscriber object title. + description: Specification subscriber object description. + Uses decorated docstring as default. + include_in_schema: Whether to include operation in Specification schema or not. + max_workers: Number of workers to process messages concurrently. + + Returns: + Union of DefaultSubscriber, BatchSubscriber, or ConcurrentDefaultSubscriber + depending on the configuration. + """ subscriber = create_subscriber( *topics, max_workers=max_workers, @@ -1253,7 +369,6 @@ def subscriber( "fetch_min_bytes": fetch_min_bytes, "max_partition_fetch_bytes": max_partition_fetch_bytes, "auto_offset_reset": auto_offset_reset, - "enable_auto_commit": auto_commit, "auto_commit_interval_ms": auto_commit_interval_ms, "check_crcs": check_crcs, "partition_assignment_strategy": partition_assignment_strategy, @@ -1262,31 +377,28 @@ def subscriber( "heartbeat_interval_ms": heartbeat_interval_ms, "isolation_level": isolation_level, }, - is_manual=not auto_commit, + auto_commit=auto_commit, # subscriber args + ack_policy=ack_policy, no_ack=no_ack, no_reply=no_reply, - retry=retry, - broker_middlewares=self._middlewares, - broker_dependencies=self._dependencies, - # AsyncAPI + config=self.config, + # Specification title_=title, description_=description, - include_in_schema=self._solve_include_in_schema(include_in_schema), + include_in_schema=include_in_schema, ) if batch: - subscriber = cast("AsyncAPIBatchSubscriber", subscriber) + subscriber = cast("BatchSubscriber", subscriber) + elif max_workers > 1: + subscriber = cast("ConcurrentDefaultSubscriber", subscriber) else: - if max_workers > 1: - subscriber = cast("AsyncAPIConcurrentDefaultSubscriber", subscriber) - else: - subscriber = cast("AsyncAPIDefaultSubscriber", subscriber) + subscriber = cast("DefaultSubscriber", subscriber) subscriber = super().subscriber(subscriber) # type: ignore[assignment] return subscriber.add_call( - filter_=filter, parser_=parser or self._parser, decoder_=decoder or self._decoder, dependencies_=dependencies, @@ -1296,326 +408,120 @@ def subscriber( @overload # type: ignore[override] def publisher( self, - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], + topic: str, *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Topic name to send response."), - ] = "", - batch: Annotated[ - Literal[False], - Doc("Whether to send messages in batches or not."), - ] = False, + key: bytes | Any | None = None, + partition: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + batch: Literal[False] = False, # basic args - middlewares: Annotated[ - Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - autoflush: Annotated[ - bool, - Doc("Whether to flush the producer or not on every publish call."), - ] = False, - ) -> "AsyncAPIDefaultPublisher": ... + middlewares: Sequence["PublisherMiddleware"] = (), + # Specification args + title: str | None = None, + description: str | None = None, + schema: Any | None = None, + include_in_schema: bool = True, + autoflush: bool = False, + ) -> "DefaultPublisher": ... @overload def publisher( self, - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], + topic: str, *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Topic name to send response."), - ] = "", - batch: Annotated[ - Literal[True], - Doc("Whether to send messages in batches or not."), - ], + key: bytes | Any | None = None, + partition: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + batch: Literal[True], # basic args - middlewares: Annotated[ - Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - autoflush: Annotated[ - bool, - Doc("Whether to flush the producer or not on every publish call."), - ] = False, - ) -> "AsyncAPIBatchPublisher": ... + middlewares: Sequence["PublisherMiddleware"] = (), + # Specification args + title: str | None = None, + description: str | None = None, + schema: Any | None = None, + include_in_schema: bool = True, + autoflush: bool = False, + ) -> "BatchPublisher": ... @overload def publisher( self, - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], + topic: str, *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Topic name to send response."), - ] = "", - batch: Annotated[ - bool, - Doc("Whether to send messages in batches or not."), - ] = False, + key: bytes | Any | None = None, + partition: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + batch: bool = False, # basic args - middlewares: Annotated[ - Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - autoflush: Annotated[ - bool, - Doc("Whether to flush the producer or not on every publish call."), - ] = False, + middlewares: Sequence["PublisherMiddleware"] = (), + # Specification args + title: str | None = None, + description: str | None = None, + schema: Any | None = None, + include_in_schema: bool = True, + autoflush: bool = False, ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", + "BatchPublisher", + "DefaultPublisher", ]: ... @override def publisher( self, - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], + topic: str, *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Topic name to send response."), - ] = "", - batch: Annotated[ - bool, - Doc("Whether to send messages in batches or not."), - ] = False, + key: bytes | Any | None = None, + partition: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + batch: bool = False, # basic args - middlewares: Annotated[ - Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - autoflush: Annotated[ - bool, - Doc("Whether to flush the producer or not on every publish call."), - ] = False, + middlewares: Sequence["PublisherMiddleware"] = (), + # Specification args + title: str | None = None, + description: str | None = None, + schema: Any | None = None, + include_in_schema: bool = True, + autoflush: bool = False, ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", + "BatchPublisher", + "DefaultPublisher", ]: - """Creates long-living and AsyncAPI-documented publisher object. + """Creates long-living and Specification-documented publisher object. You can use it as a handler decorator (handler should be decorated by `@broker.subscriber(...)` too) - `@broker.publisher(...)`. In such case publisher will publish your handler return value. Or you can create a publisher object to call it lately - `broker.publisher(...).publish(...)`. + + Args: + topic: Topic where the message will be published. + key: A key to associate with the message. Can be used to + determine which partition to send the message to. If partition + is `None` (and producer's partitioner config is left as default), + then messages with the same key will be delivered to the same + partition (but if key is `None`, partition is chosen randomly). + Must be type `bytes`, or be serializable to bytes via configured + `key_serializer`. + partition: Specify a partition. If not set, the partition will be + selected using the configured `partitioner`. + headers: Message headers to store metainformation. + **content-type** and **correlation_id** will be set automatically by framework anyway. + Can be overridden by `publish.headers` if specified. + reply_to: Topic name to send response. + batch: Whether to send messages in batches or not. + middlewares: Publisher middlewares to wrap outgoing messages. + This option was deprecated in 0.6.0. Use router-level middlewares instead. + Scheduled to remove in 0.7.0 + title: Specification publisher object title. + description: Specification publisher object description. + schema: Specification publishing message type. + Should be any python-native object annotation or `pydantic.BaseModel`. + include_in_schema: Whetever to include operation in Specification schema or not. + autoflush: Whether to flush the producer or not on every publish call. """ publisher = create_publisher( # batch flag @@ -1628,20 +534,20 @@ def publisher( headers=headers, reply_to=reply_to, # publisher-specific - broker_middlewares=self._middlewares, + config=self.config, middlewares=middlewares, - # AsyncAPI + # Specification title_=title, description_=description, schema_=schema, - include_in_schema=self._solve_include_in_schema(include_in_schema), + include_in_schema=include_in_schema, autoflush=autoflush, ) if batch: - publisher = cast("AsyncAPIBatchPublisher", publisher) + publisher = cast("BatchPublisher", publisher) else: - publisher = cast("AsyncAPIDefaultPublisher", publisher) + publisher = cast("DefaultPublisher", publisher) return super().publisher(publisher) # type: ignore[return-value,arg-type] @@ -1651,11 +557,9 @@ def include_router( router: "KafkaRegistrator", # type: ignore[override] *, prefix: str = "", - dependencies: Iterable["Depends"] = (), - middlewares: Iterable[ - "BrokerMiddleware[Union[Message, Tuple[Message, ...]]]" - ] = (), - include_in_schema: Optional[bool] = None, + dependencies: Iterable["Dependant"] = (), + middlewares: Iterable["BrokerMiddleware[Message | tuple[Message, ...]]"] = (), + include_in_schema: bool | None = None, ) -> None: if not isinstance(router, KafkaRegistrator): msg = ( diff --git a/faststream/confluent/broker/router.py b/faststream/confluent/broker/router.py new file mode 100644 index 0000000000..0711f7cc33 --- /dev/null +++ b/faststream/confluent/broker/router.py @@ -0,0 +1,360 @@ +from collections.abc import Awaitable, Callable, Iterable, Sequence +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Optional, + Union, +) + +from faststream._internal.broker.router import ( + ArgsContainer, + BrokerRouter, + SubscriberRoute, +) +from faststream._internal.constants import EMPTY +from faststream.confluent.configs import KafkaBrokerConfig +from faststream.middlewares import AckPolicy + +from .registrator import KafkaRegistrator + +if TYPE_CHECKING: + from confluent_kafka import Message + from fast_depends.dependencies import Dependant + + from faststream._internal.basic_types import SendableMessage + from faststream._internal.broker.abc_broker import Registrator + from faststream._internal.types import ( + BrokerMiddleware, + CustomCallable, + PublisherMiddleware, + SubscriberMiddleware, + ) + from faststream.confluent.message import KafkaMessage + from faststream.confluent.schemas import TopicPartition + + +class KafkaPublisher(ArgsContainer): + """Delayed KafkaPublisher registration object. + + Just a copy of `KafkaRegistrator.publisher(...)` arguments. + """ + + def __init__( + self, + topic: str, + *, + key: bytes | Any | None = None, + partition: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + batch: bool = False, + # basic args + middlewares: Sequence["PublisherMiddleware"] = (), + # AsyncAPI args + title: str | None = None, + description: str | None = None, + schema: Any | None = None, + include_in_schema: bool = True, + ) -> None: + """Initialize KafkaPublisher. + + Args: + topic: Topic where the message will be published. + key: A key to associate with the message. Can be used to + determine which partition to send the message to. If partition + is `None` (and producer's partitioner config is left as default), + then messages with the same key will be delivered to the same + partition (but if key is `None`, partition is chosen randomly). + Must be type `bytes`, or be serializable to bytes via configured + `key_serializer`. + partition: Specify a partition. If not set, the partition will be + selected using the configured `partitioner`. + headers: Message headers to store metainformation. + **content-type** and **correlation_id** will be set automatically by framework anyway. + Can be overridden by `publish.headers` if specified. + reply_to: Topic name to send response. + batch: Whether to send messages in batches or not. + middlewares: Publisher middlewares to wrap outgoing messages. + .. deprecated:: 0.6.0 + This option was deprecated in 0.6.0. Use router-level middlewares instead. + Scheduled to remove in 0.7.0 + title: AsyncAPI publisher object title. + description: AsyncAPI publisher object description. + schema: AsyncAPI publishing message type. + Should be any python-native object annotation or `pydantic.BaseModel`. + include_in_schema: Whetever to include operation in AsyncAPI schema or not. + """ + super().__init__( + topic=topic, + key=key, + partition=partition, + batch=batch, + headers=headers, + reply_to=reply_to, + # basic args + middlewares=middlewares, + # AsyncAPI args + title=title, + description=description, + schema=schema, + include_in_schema=include_in_schema, + ) + + +class KafkaRoute(SubscriberRoute): + """Class to store delaied KafkaBroker subscriber registration.""" + + def __init__( + self, + call: Callable[..., "SendableMessage"] | Callable[..., Awaitable["SendableMessage"]], + *topics: str, + publishers: Iterable[KafkaPublisher] = (), + partitions: Sequence["TopicPartition"] = (), + polling_interval: float = 0.1, + group_id: str | None = None, + group_instance_id: str | None = None, + fetch_max_wait_ms: int = 500, + fetch_max_bytes: int = 50 * 1024 * 1024, + fetch_min_bytes: int = 1, + max_partition_fetch_bytes: int = 1 * 1024 * 1024, + auto_offset_reset: Literal["latest", "earliest", "none"] = "latest", + auto_commit: bool = EMPTY, + auto_commit_interval_ms: int = 5 * 1000, + check_crcs: bool = True, + partition_assignment_strategy: Sequence[str] = ("roundrobin",), + max_poll_interval_ms: int = 5 * 60 * 1000, + session_timeout_ms: int = 10 * 1000, + heartbeat_interval_ms: int = 3 * 1000, + isolation_level: Literal["read_uncommitted", "read_committed"] = "read_uncommitted", + batch: bool = False, + max_records: int | None = None, + # broker args + dependencies: Iterable["Dependant"] = (), + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, + middlewares: Sequence["SubscriberMiddleware[KafkaMessage]"] = (), + no_ack: bool = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: bool = False, + # AsyncAPI args + title: str | None = None, + description: str | None = None, + include_in_schema: bool = True, + max_workers: int = 1, + ) -> None: + """Initialize KafkaRoute. + + Args: + call: Message handler function. + *topics: Kafka topics to consume messages from. + publishers: Kafka publishers to broadcast the handler result. + partitions: Sequence of topic partitions. + polling_interval: Polling interval in seconds. + group_id: Name of the consumer group to join for dynamic + partition assignment (if enabled), and to use for fetching and + committing offsets. If `None`, auto-partition assignment (via + group coordinator) and offset commits are disabled. + group_instance_id: A unique string that identifies the consumer instance. + If set, the consumer is treated as a static member of the group + and does not participate in consumer group management (e.g. + partition assignment, rebalances). This can be used to assign + partitions to specific consumers, rather than letting the group + assign partitions based on consumer metadata. + fetch_max_wait_ms: The maximum amount of time in milliseconds + the server will block before answering the fetch request if + there isn't sufficient data to immediately satisfy the + requirement given by `fetch_min_bytes`. + fetch_max_bytes: The maximum amount of data the server should + return for a fetch request. This is not an absolute maximum, if + the first message in the first non-empty partition of the fetch + is larger than this value, the message will still be returned + to ensure that the consumer can make progress. NOTE: consumer + performs fetches to multiple brokers in parallel so memory + usage will depend on the number of brokers containing + partitions for the topic. + fetch_min_bytes: Minimum amount of data the server should + return for a fetch request, otherwise wait up to + `fetch_max_wait_ms` for more data to accumulate. + max_partition_fetch_bytes: The maximum amount of data + per-partition the server will return. The maximum total memory + used for a request ``= #partitions * max_partition_fetch_bytes``. + This size must be at least as large as the maximum message size + the server allows or else it is possible for the producer to + send messages larger than the consumer can fetch. If that + happens, the consumer can get stuck trying to fetch a large + message on a certain partition. + auto_offset_reset: A policy for resetting offsets on `OffsetOutOfRangeError` errors: + + * `earliest` will move to the oldest available message + * `latest` will move to the most recent + * `none` will raise an exception so you can handle this case + auto_commit: If `True` the consumer's offset will be + periodically committed in the background. + auto_commit_interval_ms: Milliseconds between automatic + offset commits, if `auto_commit` is `True`. + check_crcs: Automatically check the CRC32 of the records + consumed. This ensures no on-the-wire or on-disk corruption to + the messages occurred. This check adds some overhead, so it may + be disabled in cases seeking extreme performance. + partition_assignment_strategy: List of objects to use to + distribute partition ownership amongst consumer instances when + group management is used. This preference is implicit in the order + of the strategies in the list. When assignment strategy changes: + to support a change to the assignment strategy, new versions must + enable support both for the old assignment strategy and the new + one. The coordinator will choose the old assignment strategy until + all members have been updated. Then it will choose the new + strategy. + max_poll_interval_ms: Maximum allowed time between calls to + consume messages in batches. If this interval + is exceeded the consumer is considered failed and the group will + rebalance in order to reassign the partitions to another consumer + group member. If API methods block waiting for messages, that time + does not count against this timeout. + session_timeout_ms: Client group session and failure detection + timeout. The consumer sends periodic heartbeats + (`heartbeat.interval.ms`) to indicate its liveness to the broker. + If no hearts are received by the broker for a group member within + the session timeout, the broker will remove the consumer from the + group and trigger a rebalance. The allowed range is configured with + the **broker** configuration properties + `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. + heartbeat_interval_ms: The expected time in milliseconds + between heartbeats to the consumer coordinator when using + Kafka's group management feature. Heartbeats are used to ensure + that the consumer's session stays active and to facilitate + rebalancing when new consumers join or leave the group. The + value must be set lower than `session_timeout_ms`, but typically + should be set no higher than 1/3 of that value. It can be + adjusted even lower to control the expected time for normal + rebalances. + isolation_level: Controls how to read messages written + transactionally. + + * `read_committed`, batch consumer will only return + transactional messages which have been committed. + + * `read_uncommitted` (the default), batch consumer will + return all messages, even transactional messages which have been + aborted. + + Non-transactional messages will be returned unconditionally in + either mode. + + Messages will always be returned in offset order. Hence, in + `read_committed` mode, batch consumer will only return + messages up to the last stable offset (LSO), which is the one less + than the offset of the first open transaction. In particular any + messages appearing after messages belonging to ongoing transactions + will be withheld until the relevant transaction has been completed. + As a result, `read_committed` consumers will not be able to read up + to the high watermark when there are in flight transactions. + Further, when in `read_committed` the seek_to_end method will + return the LSO. See method docs below. + batch: Whether to consume messages in batches or not. + max_records: Number of messages to consume as one batch. + dependencies: Dependencies list (`[Dependant(),]`) to apply to the subscriber. + parser: Parser to map original **Message** object to FastStream one. + decoder: Function to decode FastStream msg bytes body to python objects. + middlewares: Subscriber middlewares to wrap incoming message processing. + no_ack: Whether to disable **FastStream** auto acknowledgement logic or not. + ack_policy: Acknowledgement policy. + no_reply: Whether to disable **FastStream** RPC and Reply To auto responses or not. + title: AsyncAPI subscriber object title. + description: AsyncAPI subscriber object description. + Uses decorated docstring as default. + include_in_schema: Whetever to include operation in AsyncAPI schema or not. + max_workers: Number of workers to process messages concurrently. + """ + super().__init__( + call, + *topics, + publishers=publishers, + max_workers=max_workers, + partitions=partitions, + polling_interval=polling_interval, + group_id=group_id, + group_instance_id=group_instance_id, + fetch_max_wait_ms=fetch_max_wait_ms, + fetch_max_bytes=fetch_max_bytes, + fetch_min_bytes=fetch_min_bytes, + max_partition_fetch_bytes=max_partition_fetch_bytes, + auto_offset_reset=auto_offset_reset, + auto_commit=auto_commit, + auto_commit_interval_ms=auto_commit_interval_ms, + check_crcs=check_crcs, + partition_assignment_strategy=partition_assignment_strategy, + max_poll_interval_ms=max_poll_interval_ms, + session_timeout_ms=session_timeout_ms, + heartbeat_interval_ms=heartbeat_interval_ms, + isolation_level=isolation_level, + max_records=max_records, + batch=batch, + # basic args + dependencies=dependencies, + parser=parser, + decoder=decoder, + middlewares=middlewares, + no_reply=no_reply, + # AsyncAPI args + title=title, + description=description, + include_in_schema=include_in_schema, + ack_policy=ack_policy, + no_ack=no_ack, + ) + + +class KafkaRouter( + KafkaRegistrator, + BrokerRouter[ + Union[ + "Message", + tuple["Message", ...], + ] + ], +): + """Includable to KafkaBroker router.""" + + def __init__( + self, + prefix: str = "", + handlers: Iterable[KafkaRoute] = (), + *, + dependencies: Iterable["Dependant"] = (), + middlewares: Sequence[ + Union[ + "BrokerMiddleware[Message]", + "BrokerMiddleware[tuple[Message, ...]]", + ] + ] = (), + routers: Sequence["Registrator[Message]"] = (), + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, + include_in_schema: bool | None = None, + ) -> None: + """Initialize KafkaRouter. + + Args: + prefix: String prefix to add to all subscribers queues. + handlers: Route object to include. + dependencies: Dependencies list (`[Dependant(),]`) to apply to all routers' publishers/subscribers. + middlewares: Router middlewares to apply to all routers' publishers/subscribers. + routers: Routers to apply to broker. + parser: Parser to map original **Message** object to FastStream one. + decoder: Function to decode FastStream msg bytes body to python objects. + include_in_schema: Whetever to include operation in AsyncAPI schema or not. + """ + super().__init__( + handlers=handlers, + config=KafkaBrokerConfig( + broker_middlewares=middlewares, + broker_dependencies=dependencies, + broker_parser=parser, + broker_decoder=decoder, + include_in_schema=include_in_schema, + prefix=prefix, + ), + routers=routers, + ) diff --git a/faststream/confluent/client.py b/faststream/confluent/client.py deleted file mode 100644 index 25f7f586de..0000000000 --- a/faststream/confluent/client.py +++ /dev/null @@ -1,470 +0,0 @@ -import asyncio -import logging -from contextlib import suppress -from time import time -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, - Optional, - Sequence, - Tuple, - Union, -) - -import anyio -from confluent_kafka import Consumer, KafkaError, KafkaException, Message, Producer -from confluent_kafka.admin import AdminClient, NewTopic - -from faststream.confluent import config as config_module -from faststream.confluent.schemas import TopicPartition -from faststream.exceptions import SetupError -from faststream.log import logger as faststream_logger -from faststream.types import EMPTY -from faststream.utils.functions import call_or_await - -if TYPE_CHECKING: - from typing_extensions import NotRequired, TypedDict - - from faststream.confluent.schemas.params import SecurityOptions - from faststream.types import AnyDict, LoggerProto - - class _SendKwargs(TypedDict): - value: Optional[Union[str, bytes]] - key: Optional[Union[str, bytes]] - headers: Optional[List[Tuple[str, Union[str, bytes]]]] - partition: NotRequired[int] - timestamp: NotRequired[int] - on_delivery: NotRequired[Callable[..., None]] - - -class AsyncConfluentProducer: - """An asynchronous Python Kafka client using the "confluent-kafka" package.""" - - def __init__( - self, - *, - logger: Optional["LoggerProto"], - config: config_module.ConfluentFastConfig, - bootstrap_servers: Union[str, List[str]] = "localhost", - client_id: Optional[str] = None, - metadata_max_age_ms: int = 300000, - request_timeout_ms: int = 40000, - acks: Any = EMPTY, - compression_type: Optional[str] = None, - partitioner: str = "consistent_random", - max_request_size: int = 1048576, - linger_ms: int = 0, - retry_backoff_ms: int = 100, - security_protocol: str = "PLAINTEXT", - connections_max_idle_ms: int = 540000, - enable_idempotence: bool = False, - transactional_id: Optional[Union[str, int]] = None, - transaction_timeout_ms: int = 60000, - allow_auto_create_topics: bool = True, - security_config: Optional["SecurityOptions"] = None, - ) -> None: - self.logger = logger - - if isinstance(bootstrap_servers, Iterable) and not isinstance( - bootstrap_servers, str - ): - bootstrap_servers = ",".join(bootstrap_servers) - - if compression_type is None: - compression_type = "none" - - if acks is EMPTY or acks == "all": - acks = -1 - - config_from_params = { - # "topic.metadata.refresh.interval.ms": 1000, - "bootstrap.servers": bootstrap_servers, - "client.id": client_id, - "metadata.max.age.ms": metadata_max_age_ms, - "request.timeout.ms": request_timeout_ms, - "acks": acks, - "compression.type": compression_type, - "partitioner": partitioner, - "message.max.bytes": max_request_size, - "linger.ms": linger_ms, - "enable.idempotence": enable_idempotence, - "transactional.id": transactional_id, - "transaction.timeout.ms": transaction_timeout_ms, - "retry.backoff.ms": retry_backoff_ms, - "security.protocol": security_protocol.lower(), - "connections.max.idle.ms": connections_max_idle_ms, - "allow.auto.create.topics": allow_auto_create_topics, - } - - self.config = { - **config_from_params, - **dict(security_config or {}), - **config.as_config_dict(), - } - - self.producer = Producer(self.config, logger=self.logger) # type: ignore[call-arg] - - self.__running = True - self._poll_task = asyncio.create_task(self._poll_loop()) - - async def _poll_loop(self) -> None: - while self.__running: - with suppress(Exception): - await call_or_await(self.producer.poll, 0.1) - - async def stop(self) -> None: - """Stop the Kafka producer and flush remaining messages.""" - if self.__running: - self.__running = False - if not self._poll_task.done(): - self._poll_task.cancel() - await call_or_await(self.producer.flush) - - async def flush(self) -> None: - await call_or_await(self.producer.flush) - - async def send( - self, - topic: str, - value: Optional[Union[str, bytes]] = None, - key: Optional[Union[str, bytes]] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[List[Tuple[str, Union[str, bytes]]]] = None, - no_confirm: bool = False, - ) -> None: - """Sends a single message to a Kafka topic.""" - kwargs: _SendKwargs = { - "value": value, - "key": key, - "headers": headers, - } - - if partition is not None: - kwargs["partition"] = partition - - if timestamp_ms is not None: - kwargs["timestamp"] = timestamp_ms - - if not no_confirm: - result_future: asyncio.Future[Optional[Message]] = asyncio.Future() - - def ack_callback(err: Any, msg: Optional[Message]) -> None: - if err or (msg is not None and (err := msg.error())): - result_future.set_exception(KafkaException(err)) - else: - result_future.set_result(msg) - - kwargs["on_delivery"] = ack_callback - - # should be sync to prevent segfault - self.producer.produce(topic, **kwargs) - - if not no_confirm: - await result_future - - def create_batch(self) -> "BatchBuilder": - """Creates a batch for sending multiple messages.""" - return BatchBuilder() - - async def send_batch( - self, - batch: "BatchBuilder", - topic: str, - *, - partition: Optional[int], - no_confirm: bool = False, - ) -> None: - """Sends a batch of messages to a Kafka topic.""" - async with anyio.create_task_group() as tg: - for msg in batch._builder: - tg.start_soon( - self.send, - topic, - msg["value"], - msg["key"], - partition, - msg["timestamp_ms"], - msg["headers"], - no_confirm, - ) - - async def ping( - self, - timeout: Optional[float] = 5.0, - ) -> bool: - """Implement ping using `list_topics` information request.""" - if timeout is None: - timeout = -1 - - try: - cluster_metadata = await call_or_await( - self.producer.list_topics, - timeout=timeout, - ) - - return bool(cluster_metadata) - - except Exception: - return False - - -class AsyncConfluentConsumer: - """An asynchronous Python Kafka client for consuming messages using the "confluent-kafka" package.""" - - def __init__( - self, - *topics: str, - partitions: Sequence["TopicPartition"], - logger: Optional["LoggerProto"], - config: config_module.ConfluentFastConfig, - bootstrap_servers: Union[str, List[str]] = "localhost", - client_id: Optional[str] = "confluent-kafka-consumer", - group_id: Optional[str] = None, - group_instance_id: Optional[str] = None, - fetch_max_wait_ms: int = 500, - fetch_max_bytes: int = 52428800, - fetch_min_bytes: int = 1, - max_partition_fetch_bytes: int = 1 * 1024 * 1024, - retry_backoff_ms: int = 100, - auto_offset_reset: str = "latest", - enable_auto_commit: bool = True, - auto_commit_interval_ms: int = 5000, - check_crcs: bool = True, - metadata_max_age_ms: int = 5 * 60 * 1000, - partition_assignment_strategy: Union[str, List[Any]] = "roundrobin", - max_poll_interval_ms: int = 300000, - session_timeout_ms: int = 10000, - heartbeat_interval_ms: int = 3000, - security_protocol: str = "PLAINTEXT", - connections_max_idle_ms: int = 540000, - isolation_level: str = "read_uncommitted", - allow_auto_create_topics: bool = True, - security_config: Optional["SecurityOptions"] = None, - ) -> None: - self.logger = logger - - if isinstance(bootstrap_servers, Iterable) and not isinstance( - bootstrap_servers, str - ): - bootstrap_servers = ",".join(bootstrap_servers) - - self.topics = list(topics) - self.partitions = partitions - - if not isinstance(partition_assignment_strategy, str): - partition_assignment_strategy = ",".join( - [ - x if isinstance(x, str) else x().name - for x in partition_assignment_strategy - ] - ) - - config_from_params = { - "allow.auto.create.topics": allow_auto_create_topics, - "topic.metadata.refresh.interval.ms": 1000, - "bootstrap.servers": bootstrap_servers, - "client.id": client_id, - "group.id": config.config.get( - "group.id", group_id or "faststream-consumer-group" - ), - "group.instance.id": config.config.get( - "group.instance.id", group_instance_id - ), - "fetch.wait.max.ms": fetch_max_wait_ms, - "fetch.max.bytes": fetch_max_bytes, - "fetch.min.bytes": fetch_min_bytes, - "max.partition.fetch.bytes": max_partition_fetch_bytes, - "fetch.error.backoff.ms": retry_backoff_ms, - "auto.offset.reset": auto_offset_reset, - "enable.auto.commit": enable_auto_commit, - "auto.commit.interval.ms": auto_commit_interval_ms, - "check.crcs": check_crcs, - "metadata.max.age.ms": metadata_max_age_ms, - "partition.assignment.strategy": partition_assignment_strategy, - "max.poll.interval.ms": max_poll_interval_ms, - "session.timeout.ms": session_timeout_ms, - "heartbeat.interval.ms": heartbeat_interval_ms, - "security.protocol": security_protocol.lower(), - "connections.max.idle.ms": connections_max_idle_ms, - "isolation.level": isolation_level, - } - self.allow_auto_create_topics = allow_auto_create_topics - - self.config = { - **config_from_params, - **dict(security_config or {}), - **config.as_config_dict(), - } - self.consumer = Consumer(self.config, logger=self.logger) # type: ignore[call-arg] - - # We shouldn't read messages and close consumer concurrently - # https://github.com/ag2ai/faststream/issues/1904#issuecomment-2506990895 - self._lock = anyio.Lock() - - @property - def topics_to_create(self) -> List[str]: - return list({*self.topics, *(p.topic for p in self.partitions)}) - - async def start(self) -> None: - """Starts the Kafka consumer and subscribes to the specified topics.""" - if self.allow_auto_create_topics: - await call_or_await( - create_topics, self.topics_to_create, self.config, self.logger - ) - - elif self.logger: - self.logger.log( - logging.WARNING, - "Auto create topics is disabled. Make sure the topics exist.", - ) - - if self.topics: - await call_or_await(self.consumer.subscribe, self.topics) - - elif self.partitions: - await call_or_await( - self.consumer.assign, [p.to_confluent() for p in self.partitions] - ) - - else: - raise SetupError("You must provide either `topics` or `partitions` option.") - - async def commit(self, asynchronous: bool = True) -> None: - """Commits the offsets of all messages returned by the last poll operation.""" - await call_or_await(self.consumer.commit, asynchronous=asynchronous) - - async def stop(self) -> None: - """Stops the Kafka consumer and releases all resources.""" - # NOTE: If we don't explicitly call commit and then close the consumer, the confluent consumer gets stuck. - # We are doing this to avoid the issue. - enable_auto_commit = self.config["enable.auto.commit"] - try: - if enable_auto_commit: - await self.commit(asynchronous=False) - - except Exception as e: - # No offset stored issue is not a problem - https://github.com/confluentinc/confluent-kafka-python/issues/295#issuecomment-355907183 - if "No offset stored" in str(e): - pass - elif self.logger: - self.logger.log( - logging.ERROR, - "Consumer closing error occurred.", - exc_info=e, - ) - - # Wrap calls to async to make method cancelable by timeout - async with self._lock: - await call_or_await(self.consumer.close) - - async def getone(self, timeout: float = 0.1) -> Optional[Message]: - """Consumes a single message from Kafka.""" - async with self._lock: - msg = await call_or_await(self.consumer.poll, timeout) - return check_msg_error(msg) - - async def getmany( - self, - timeout: float = 0.1, - max_records: Optional[int] = 10, - ) -> Tuple[Message, ...]: - """Consumes a batch of messages from Kafka and groups them by topic and partition.""" - async with self._lock: - raw_messages: List[Optional[Message]] = await call_or_await( - self.consumer.consume, # type: ignore[arg-type] - num_messages=max_records or 10, - timeout=timeout, - ) - - return tuple(x for x in map(check_msg_error, raw_messages) if x is not None) - - async def seek(self, topic: str, partition: int, offset: int) -> None: - """Seeks to the specified offset in the specified topic and partition.""" - topic_partition = TopicPartition( - topic=topic, partition=partition, offset=offset - ) - await call_or_await(self.consumer.seek, topic_partition.to_confluent()) - - -def check_msg_error(msg: Optional[Message]) -> Optional[Message]: - """Checks for errors in the consumed message.""" - if msg is None or msg.error(): - return None - - return msg - - -class BatchBuilder: - """A helper class to build a batch of messages to send to Kafka.""" - - def __init__(self) -> None: - """Initializes a new BatchBuilder instance.""" - self._builder: List[AnyDict] = [] - - def append( - self, - *, - timestamp: Optional[int] = None, - key: Optional[Union[str, bytes]] = None, - value: Optional[Union[str, bytes]] = None, - headers: Optional[List[Tuple[str, bytes]]] = None, - ) -> None: - """Appends a message to the batch with optional timestamp, key, value, and headers.""" - if key is None and value is None: - raise KafkaException( - KafkaError(40, reason="Both key and value can't be None") - ) - - self._builder.append( - { - "timestamp_ms": timestamp or round(time() * 1000), - "key": key, - "value": value, - "headers": headers or [], - } - ) - - -def create_topics( - topics: List[str], - config: Dict[str, Optional[Union[str, int, float, bool, Any]]], - logger_: Optional["LoggerProto"] = None, -) -> None: - logger_ = logger_ or faststream_logger - - """Creates Kafka topics using the provided configuration.""" - admin_client = AdminClient( - {x: config[x] for x in ADMINCLIENT_CONFIG_PARAMS if x in config} - ) - - fs = admin_client.create_topics( - [NewTopic(topic, num_partitions=1, replication_factor=1) for topic in topics] - ) - - for topic, f in fs.items(): - try: - f.result() # The result itself is None - except Exception as e: # noqa: PERF203 - if "TOPIC_ALREADY_EXISTS" not in str(e): - logger_.log(logging.WARN, f"Failed to create topic {topic}: {e}") - else: - logger_.log(logging.INFO, f"Topic `{topic}` created.") - - -ADMINCLIENT_CONFIG_PARAMS = ( - "allow.auto.create.topics", - "bootstrap.servers", - "client.id", - "request.timeout.ms", - "metadata.max.age.ms", - "security.protocol", - "connections.max.idle.ms", - "sasl.mechanism", - "sasl.username", - "sasl.password", -) diff --git a/faststream/confluent/config.py b/faststream/confluent/config.py deleted file mode 100644 index d07e48f608..0000000000 --- a/faststream/confluent/config.py +++ /dev/null @@ -1,295 +0,0 @@ -from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, Optional, Union - -from typing_extensions import TypedDict - -if TYPE_CHECKING: - from faststream.types import AnyDict - - -class BuiltinFeatures(str, Enum): - gzip = "gzip" - snappy = "snappy" - ssl = "ssl" - sasl = "sasl" - regex = "regex" - lz4 = "lz4" - sasl_gssapi = "sasl_gssapi" - sasl_plain = "sasl_plain" - sasl_scram = "sasl_scram" - plugins = "plugins" - zstd = "zstd" - sasl_oauthbearer = "sasl_oauthbearer" - http = "http" - oidc = "oidc" - - -class Debug(str, Enum): - generic = "generic" - broker = "broker" - topic = "topic" - metadata = "metadata" - feature = "feature" - queue = "queue" - msg = "msg" - protocol = "protocol" - cgrp = "cgrp" - security = "security" - fetch = "fetch" - interceptor = "interceptor" - plugin = "plugin" - consumer = "consumer" - admin = "admin" - eos = "eos" - mock = "mock" - assignor = "assignor" - conf = "conf" - all = "all" - - -class BrokerAddressFamily(str, Enum): - any = "any" - v4 = "v4" - v6 = "v6" - - -class SecurityProtocol(str, Enum): - plaintext = "plaintext" - ssl = "ssl" - sasl_plaintext = "sasl_plaintext" - sasl_ssl = "sasl_ssl" - - -class SASLOAUTHBearerMethod(str, Enum): - default = "default" - oidc = "oidc" - - -class GroupProtocol(str, Enum): - classic = "classic" - consumer = "consumer" - - -class OffsetStoreMethod(str, Enum): - none = "none" - file = "file" - broker = "broker" - - -class IsolationLevel(str, Enum): - read_uncommitted = "read_uncommitted" - read_committed = "read_committed" - - -class CompressionCodec(str, Enum): - none = "none" - gzip = "gzip" - snappy = "snappy" - lz4 = "lz4" - zstd = "zstd" - - -class CompressionType(str, Enum): - none = "none" - gzip = "gzip" - snappy = "snappy" - lz4 = "lz4" - zstd = "zstd" - - -class ClientDNSLookup(str, Enum): - use_all_dns_ips = "use_all_dns_ips" - resolve_canonical_bootstrap_servers_only = ( - "resolve_canonical_bootstrap_servers_only" - ) - - -ConfluentConfig = TypedDict( - "ConfluentConfig", - { - "compression.codec": Union[CompressionCodec, str], - "compression.type": Union[CompressionType, str], - "client.dns.lookup": Union[ClientDNSLookup, str], - "offset.store.method": Union[OffsetStoreMethod, str], - "isolation.level": Union[IsolationLevel, str], - "sasl.oauthbearer.method": Union[SASLOAUTHBearerMethod, str], - "security.protocol": Union[SecurityProtocol, str], - "broker.address.family": Union[BrokerAddressFamily, str], - "builtin.features": Union[BuiltinFeatures, str], - "debug": Union[Debug, str], - "group.protocol": Union[GroupProtocol, str], - "client.id": str, - "metadata.broker.list": str, - "bootstrap.servers": str, - "message.max.bytes": int, - "message.copy.max.bytes": int, - "receive.message.max.bytes": int, - "max.in.flight.requests.per.connection": int, - "max.in.flight": int, - "topic.metadata.refresh.interval.ms": int, - "metadata.max.age.ms": int, - "topic.metadata.refresh.fast.interval.ms": int, - "topic.metadata.refresh.fast.cnt": int, - "topic.metadata.refresh.sparse": bool, - "topic.metadata.propagation.max.ms": int, - "topic.blacklist": str, - "socket.timeout.ms": int, - "socket.blocking.max.ms": int, - "socket.send.buffer.bytes": int, - "socket.receive.buffer.bytes": int, - "socket.keepalive.enable": bool, - "socket.nagle.disable": bool, - "socket.max.fails": int, - "broker.address.ttl": int, - "socket.connection.setup.timeout.ms": int, - "connections.max.idle.ms": int, - "reconnect.backoff.jitter.ms": int, - "reconnect.backoff.ms": int, - "reconnect.backoff.max.ms": int, - "statistics.interval.ms": int, - "enabled_events": int, - "error_cb": Callable[..., Any], - "throttle_cb": Callable[..., Any], - "stats_cb": Callable[..., Any], - "log_cb": Callable[..., Any], - "log_level": int, - "log.queue": bool, - "log.thread.name": bool, - "enable.random.seed": bool, - "log.connection.close": bool, - "background_event_cb": Callable[..., Any], - "socket_cb": Callable[..., Any], - "connect_cb": Callable[..., Any], - "closesocket_cb": Callable[..., Any], - "open_cb": Callable[..., Any], - "resolve_cb": Callable[..., Any], - "opaque": str, - "default_topic_conf": str, - "internal.termination.signal": int, - "api.version.request": bool, - "api.version.request.timeout.ms": int, - "api.version.fallback.ms": int, - "broker.version.fallback": str, - "allow.auto.create.topics": bool, - "ssl.cipher.suites": str, - "ssl.curves.list": str, - "ssl.sigalgs.list": str, - "ssl.key.location": str, - "ssl.key.password": str, - "ssl.key.pem": str, - "ssl_key": str, - "ssl.certificate.location": str, - "ssl.certificate.pem": str, - "ssl_certificate": str, - "ssl.ca.location": str, - "ssl.ca.pem": str, - "ssl_ca": str, - "ssl.ca.certificate.stores": str, - "ssl.crl.location": str, - "ssl.keystore.location": str, - "ssl.keystore.password": str, - "ssl.providers": str, - "ssl.engine.location": str, - "ssl.engine.id": str, - "ssl_engine_callback_data": str, - "enable.ssl.certificate.verification": bool, - "ssl.endpoint.identification.algorithm": str, - "ssl.certificate.verify_cb": Callable[..., Any], - "sasl.mechanisms": str, - "sasl.mechanism": str, - "sasl.kerberos.service.name": str, - "sasl.kerberos.principal": str, - "sasl.kerberos.kinit.cmd": str, - "sasl.kerberos.keytab": str, - "sasl.kerberos.min.time.before.relogin": int, - "sasl.username": str, - "sasl.password": str, - "sasl.oauthbearer.config": str, - "enable.sasl.oauthbearer.unsecure.jwt": bool, - "oauth_cb": Callable[..., Any], - "sasl.oauthbearer.client.id": str, - "sasl.oauthbearer.client.secret": str, - "sasl.oauthbearer.scope": str, - "sasl.oauthbearer.extensions": str, - "sasl.oauthbearer.token.endpoint.url": str, - "plugin.library.paths": str, - "interceptors": str, - "group.id": str, - "group.instance.id": str, - "partition.assignment.strategy": str, - "session.timeout.ms": str, - "heartbeat.interval.ms": str, - "group.protocol.type": str, - "group.remote.assignor": str, - "coordinator.query.interval.ms": int, - "max.poll.interval.ms": int, - "enable.auto.commit": bool, - "auto.commit.interval.ms": int, - "enable.auto.offset.store": bool, - "queued.min.messages": int, - "queued.max.messages.kbytes": int, - "fetch.wait.max.ms": int, - "fetch.queue.backoff.ms": int, - "fetch.message.max.bytes": int, - "max.partition.fetch.bytes": int, - "fetch.max.bytes": int, - "fetch.min.bytes": int, - "fetch.error.backoff.ms": int, - "consume_cb": Callable[..., Any], - "rebalance_cb": Callable[..., Any], - "offset_commit_cb": Callable[..., Any], - "enable.partition.eof": bool, - "check.crcs": bool, - "client.rack": str, - "transactional.id": str, - "transaction.timeout.ms": int, - "enable.idempotence": bool, - "enable.gapless.guarantee": bool, - "queue.buffering.max.messages": int, - "queue.buffering.max.kbytes": int, - "queue.buffering.max.ms": float, - "linger.ms": float, - "message.send.max.retries": int, - "retries": int, - "retry.backoff.ms": int, - "retry.backoff.max.ms": int, - "queue.buffering.backpressure.threshold": int, - "batch.num.messages": int, - "batch.size": int, - "delivery.report.only.error": bool, - "dr_cb": Callable[..., Any], - "dr_msg_cb": Callable[..., Any], - "sticky.partitioning.linger.ms": int, - "on_delivery": Callable[..., Any], - }, - total=False, -) - - -class ConfluentFastConfig: - def __init__(self, config: Optional[ConfluentConfig]) -> None: - self.config = config or {} - - def as_config_dict(self) -> "AnyDict": - if not self.config: - return {} - - data = dict(self.config) - - for key, enum in ( - ("compression.codec", CompressionCodec), - ("compression.type", CompressionType), - ("client.dns.lookup", ClientDNSLookup), - ("offset.store.method", OffsetStoreMethod), - ("isolation.level", IsolationLevel), - ("sasl.oauthbearer.method", SASLOAUTHBearerMethod), - ("security.protocol", SecurityProtocol), - ("broker.address.family", BrokerAddressFamily), - ("builtin.features", BuiltinFeatures), - ("debug", Debug), - ("group.protocol", GroupProtocol), - ): - if key in data: - data[key] = enum(data[key]).value - - return data diff --git a/faststream/confluent/configs/__init__.py b/faststream/confluent/configs/__init__.py new file mode 100644 index 0000000000..1794a6ebba --- /dev/null +++ b/faststream/confluent/configs/__init__.py @@ -0,0 +1,5 @@ +from .broker import KafkaBrokerConfig + +__all__ = ( + "KafkaBrokerConfig", +) diff --git a/faststream/confluent/configs/broker.py b/faststream/confluent/configs/broker.py new file mode 100644 index 0000000000..9a9708f841 --- /dev/null +++ b/faststream/confluent/configs/broker.py @@ -0,0 +1,69 @@ +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +from faststream.__about__ import SERVICE_NAME +from faststream._internal.configs import BrokerConfig +from faststream.confluent.helpers import ( + AdminService, + AsyncConfluentConsumer, + AsyncConfluentProducer, + ConfluentFastConfig, +) +from faststream.confluent.publisher.producer import ( + AsyncConfluentFastProducer, + FakeConfluentFastProducer, +) + +if TYPE_CHECKING: + from faststream._internal.logger import LoggerState + + +@dataclass +class ConsumerBuilder: + config: "ConfluentFastConfig" + admin: "AdminService" + logger: "LoggerState" + + def __call__(self, *topics: str, **kwargs: Any) -> "AsyncConfluentConsumer": + return AsyncConfluentConsumer( + *topics, + config=self.config, + admin_service=self.admin, + logger=self.logger, + **kwargs, + ) + + +@dataclass(kw_only=True) +class KafkaBrokerConfig(BrokerConfig): + connection_config: "ConfluentFastConfig" = field( + default_factory=ConfluentFastConfig + ) + + admin: "AdminService" = field(default_factory=AdminService) + client_id: str | None = SERVICE_NAME + + builder: Callable[..., AsyncConfluentConsumer] = field(init=False) + producer: "AsyncConfluentFastProducer" = field( + default_factory=FakeConfluentFastProducer + ) + + def __post_init__(self) -> None: + self.builder = ConsumerBuilder( + config=self.connection_config, + admin=self.admin, + logger=self.logger, + ) + + async def connect(self) -> "None": + native_producer = AsyncConfluentProducer( + config=self.connection_config, + logger=self.logger, + ) + self.producer.connect(native_producer, serializer=self.fd_config._serializer) + await self.admin.connect(self.connection_config) + + async def disconnect(self) -> "None": + await self.producer.disconnect() + await self.admin.disconnect() diff --git a/faststream/confluent/fastapi/__init__.py b/faststream/confluent/fastapi/__init__.py index 352142194a..21354fcf98 100644 --- a/faststream/confluent/fastapi/__init__.py +++ b/faststream/confluent/fastapi/__init__.py @@ -1,11 +1,12 @@ -from typing_extensions import Annotated +from typing import Annotated -from faststream.broker.fastapi.context import Context, ContextRepo, Logger +from faststream._internal.fastapi.context import Context, ContextRepo, Logger from faststream.confluent.broker import KafkaBroker as KB -from faststream.confluent.fastapi.fastapi import KafkaRouter from faststream.confluent.message import KafkaMessage as KM from faststream.confluent.publisher.producer import AsyncConfluentFastProducer +from .fastapi import KafkaRouter + __all__ = ( "Context", "ContextRepo", diff --git a/faststream/confluent/fastapi/fastapi.py b/faststream/confluent/fastapi/fastapi.py index e62b042fdf..ad52774196 100644 --- a/faststream/confluent/fastapi/fastapi.py +++ b/faststream/confluent/fastapi/fastapi.py @@ -1,16 +1,11 @@ import logging +from collections.abc import Callable, Iterable, Sequence from typing import ( TYPE_CHECKING, + Annotated, Any, - Callable, - Dict, - Iterable, - List, Literal, Optional, - Sequence, - Tuple, - Type, TypeVar, Union, cast, @@ -23,13 +18,13 @@ from fastapi.utils import generate_unique_id from starlette.responses import JSONResponse, Response from starlette.routing import BaseRoute -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import deprecated, override from faststream.__about__ import SERVICE_NAME -from faststream.broker.fastapi.router import StreamRouter -from faststream.broker.utils import default_filter -from faststream.confluent.broker.broker import KafkaBroker as KB -from faststream.types import EMPTY +from faststream._internal.constants import EMPTY +from faststream._internal.fastapi.router import StreamRouter +from faststream.confluent.broker import KafkaBroker as KB +from faststream.middlewares import AckPolicy if TYPE_CHECKING: from enum import Enum @@ -38,34 +33,33 @@ from fastapi.types import IncEx from starlette.types import ASGIApp, Lifespan - from faststream.asyncapi import schema as asyncapi - from faststream.broker.types import ( + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, - Filter, PublisherMiddleware, SubscriberMiddleware, ) - from faststream.confluent.config import ConfluentConfig + from faststream.confluent.helpers.config import ConfluentConfig from faststream.confluent.message import KafkaMessage - from faststream.confluent.publisher.asyncapi import ( - AsyncAPIBatchPublisher, - AsyncAPIDefaultPublisher, + from faststream.confluent.publisher.usecase import ( + BatchPublisher, + DefaultPublisher, ) from faststream.confluent.schemas import TopicPartition - from faststream.confluent.subscriber.asyncapi import ( - AsyncAPIBatchSubscriber, - AsyncAPIConcurrentDefaultSubscriber, - AsyncAPIDefaultSubscriber, + from faststream.confluent.subscriber.usecase import ( + BatchSubscriber, + ConcurrentDefaultSubscriber, + DefaultSubscriber, ) from faststream.security import BaseSecurity - from faststream.types import AnyDict, LoggerProto + from faststream.specification.schema.extra import Tag, TagDict Partition = TypeVar("Partition") -class KafkaRouter(StreamRouter[Union[Message, Tuple[Message, ...]]]): +class KafkaRouter(StreamRouter[Union[Message, tuple[Message, ...]]]): """A class to represent a Kafka router.""" broker_class = KB @@ -73,481 +67,170 @@ class KafkaRouter(StreamRouter[Union[Message, Tuple[Message, ...]]]): def __init__( self, - bootstrap_servers: Annotated[ - Union[str, Iterable[str]], - Doc( - """ - A `host[:port]` string (or list of `host[:port]` strings) that the consumer should contact to bootstrap - initial cluster metadata. - - This does not have to be the full node list. - It just needs to have at least one broker that will respond to a - Metadata API Request. Default port is 9092. - """ - ), - ] = "localhost", + bootstrap_servers: str | Iterable[str] = "localhost", *, # both - request_timeout_ms: Annotated[ - int, - Doc("Client request timeout in milliseconds."), - ] = 40 * 1000, - retry_backoff_ms: Annotated[ - int, - Doc("Milliseconds to backoff when retrying on errors."), - ] = 100, - metadata_max_age_ms: Annotated[ - int, - Doc( - """ - The period of time in milliseconds after - which we force a refresh of metadata even if we haven't seen any - partition leadership changes to proactively discover any new - brokers or partitions. - """ - ), - ] = 5 * 60 * 1000, - connections_max_idle_ms: Annotated[ - int, - Doc( - """ - Close idle connections after the number - of milliseconds specified by this config. Specifying `None` will - disable idle checks. - """ - ), - ] = 9 * 60 * 1000, - client_id: Annotated[ - Optional[str], - Doc( - """ - A name for this client. This string is passed in - each request to servers and can be used to identify specific - server-side log entries that correspond to this client. Also - submitted to :class:`~.consumer.group_coordinator.GroupCoordinator` - for logging with respect to consumer group administration. - """ - ), - ] = SERVICE_NAME, - allow_auto_create_topics: Annotated[ - bool, - Doc( - """ - Allow automatic topic creation on the broker when subscribing to or assigning non-existent topics. - """ - ), - ] = True, - config: Annotated[ - Optional["ConfluentConfig"], - Doc( - """ - Extra configuration for the confluent-kafka-python - producer/consumer. See `confluent_kafka.Config `_. - """ - ), - ] = None, + request_timeout_ms: int = 40 * 1000, + retry_backoff_ms: int = 100, + metadata_max_age_ms: int = 5 * 60 * 1000, + connections_max_idle_ms: int = 9 * 60 * 1000, + client_id: str | None = SERVICE_NAME, + allow_auto_create_topics: bool = True, + config: Optional["ConfluentConfig"] = None, # publisher args - acks: Annotated[ - Literal[0, 1, -1, "all"], - Doc( - """ - One of ``0``, ``1``, ``all``. The number of acknowledgments - the producer requires the leader to have received before considering a - request complete. This controls the durability of records that are - sent. The following settings are common: - - * ``0``: Producer will not wait for any acknowledgment from the server - at all. The message will immediately be added to the socket - buffer and considered sent. No guarantee can be made that the - server has received the record in this case, and the retries - configuration will not take effect (as the client won't - generally know of any failures). The offset given back for each - record will always be set to -1. - * ``1``: The broker leader will write the record to its local log but - will respond without awaiting full acknowledgement from all - followers. In this case should the leader fail immediately - after acknowledging the record but before the followers have - replicated it then the record will be lost. - * ``all``: The broker leader will wait for the full set of in-sync - replicas to acknowledge the record. This guarantees that the - record will not be lost as long as at least one in-sync replica - remains alive. This is the strongest available guarantee. - - If unset, defaults to ``acks=1``. If `enable_idempotence` is - :data:`True` defaults to ``acks=all``. - """ - ), - ] = EMPTY, - compression_type: Annotated[ - Optional[Literal["gzip", "snappy", "lz4", "zstd"]], - Doc( - """ - The compression type for all data generated bythe producer. - Compression is of full batches of data, so the efficacy of batching - will also impact the compression ratio (more batching means better - compression). - """ - ), - ] = None, - partitioner: Annotated[ - Union[ - str, - Callable[ - [bytes, List[Partition], List[Partition]], - Partition, - ], - ], - Doc( - """ - Callable used to determine which partition - each message is assigned to. Called (after key serialization): - ``partitioner(key_bytes, all_partitions, available_partitions)``. - The default partitioner implementation hashes each non-None key - using the same murmur2 algorithm as the Java client so that - messages with the same key are assigned to the same partition. - When a key is :data:`None`, the message is delivered to a random partition - (filtered to partitions with available leaders only, if possible). - """ - ), + acks: Literal[0, 1, -1, "all"] = EMPTY, + compression_type: Literal["gzip", "snappy", "lz4", "zstd"] | None = None, + partitioner: str + | Callable[ + [bytes, list[Partition], list[Partition]], Partition ] = "consistent_random", - max_request_size: Annotated[ - int, - Doc( - """ - The maximum size of a request. This is also - effectively a cap on the maximum record size. Note that the server - has its own cap on record size which may be different from this. - This setting will limit the number of record batches the producer - will send in a single request to avoid sending huge requests. - """ - ), - ] = 1024 * 1024, - linger_ms: Annotated[ - int, - Doc( - """ - The producer groups together any records that arrive - in between request transmissions into a single batched request. - Normally this occurs only under load when records arrive faster - than they can be sent out. However in some circumstances the client - may want to reduce the number of requests even under moderate load. - This setting accomplishes this by adding a small amount of - artificial delay; that is, if first request is processed faster, - than `linger_ms`, producer will wait ``linger_ms - process_time``. - """ - ), - ] = 0, - enable_idempotence: Annotated[ - bool, - Doc( - """ - When set to `True`, the producer will - ensure that exactly one copy of each message is written in the - stream. If `False`, producer retries due to broker failures, - etc., may write duplicates of the retried message in the stream. - Note that enabling idempotence acks to set to ``all``. If it is not - explicitly set by the user it will be chosen. - """ - ), - ] = False, - transactional_id: Optional[str] = None, + max_request_size: int = 1024 * 1024, + linger_ms: int = 0, + enable_idempotence: bool = False, + transactional_id: str | None = None, transaction_timeout_ms: int = 60 * 1000, # broker base args - graceful_timeout: Annotated[ - Optional[float], - Doc( - "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down." - ), - ] = 15.0, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Custom decoder object."), - ] = None, - parser: Annotated[ - Optional["CustomCallable"], - Doc("Custom parser object."), - ] = None, - middlewares: Annotated[ - Sequence[ - Union[ - "BrokerMiddleware[Message]", - "BrokerMiddleware[Tuple[Message, ...]]", - ] - ], - Doc("Middlewares to apply to all broker publishers/subscribers."), + graceful_timeout: float | None = 15.0, + decoder: Optional["CustomCallable"] = None, + parser: Optional["CustomCallable"] = None, + middlewares: Sequence[ + Union[ + "BrokerMiddleware[Message]", + "BrokerMiddleware[tuple[Message, ...]]", + ] ] = (), - # AsyncAPI args - security: Annotated[ - Optional["BaseSecurity"], - Doc( - "Security options to connect broker and generate AsyncAPI server security information." - ), - ] = None, - asyncapi_url: Annotated[ - Optional[str], - Doc("AsyncAPI hardcoded server addresses. Use `servers` if not specified."), - ] = None, - protocol: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol."), - ] = None, - protocol_version: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol version."), - ] = "auto", - description: Annotated[ - Optional[str], - Doc("AsyncAPI server description."), - ] = None, - asyncapi_tags: Annotated[ - Optional[Iterable[Union["asyncapi.Tag", "asyncapi.TagDict"]]], - Doc("AsyncAPI server tags."), - ] = None, + # Specification args + security: Optional["BaseSecurity"] = None, + specification_url: str | None = None, + protocol: str | None = None, + protocol_version: str = "auto", + description: str | None = None, + specification_tags: Iterable[Union["Tag", "TagDict"]] = (), # logging args - logger: Annotated[ - Optional["LoggerProto"], - Doc("User specified logger to pass into Context and log service messages."), - ] = EMPTY, - log_level: Annotated[ - int, - Doc("Service messages log level."), - ] = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - Doc("Default logger log format."), - ] = EMPTY, + logger: Optional["LoggerProto"] = EMPTY, + log_level: int = logging.INFO, # StreamRouter options - setup_state: Annotated[ - bool, - Doc( - "Whether to add broker to app scope in lifespan. " - "You should disable this option at old ASGI servers." - ), - ] = True, - schema_url: Annotated[ - Optional[str], - Doc( - "AsyncAPI schema url. You should set this option to `None` to disable AsyncAPI routes at all." - ), - ] = "/asyncapi", + setup_state: bool = True, + schema_url: str | None = "/asyncapi", # FastAPI args - prefix: Annotated[ - str, - Doc("An optional path prefix for the router."), - ] = "", - tags: Annotated[ - Optional[List[Union[str, "Enum"]]], - Doc( - """ - A list of tags to be applied to all the *path operations* in this - router. - - It will be added to the generated OpenAPI (e.g. visible at `/docs`). - - Read more about it in the - [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). - """ - ), - ] = None, - dependencies: Annotated[ - Optional[Sequence["params.Depends"]], - Doc( - """ - A list of dependencies (using `Depends()`) to be applied to all the - *path and stream operations* in this router. - - Read more about it in the - [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). - """ - ), - ] = None, - default_response_class: Annotated[ - Type["Response"], - Doc( - """ - The default response class to be used. - - Read more in the - [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). - """ - ), - ] = Default(JSONResponse), - responses: Annotated[ - Optional[Dict[Union[int, str], "AnyDict"]], - Doc( - """ - Additional responses to be shown in OpenAPI. - - It will be added to the generated OpenAPI (e.g. visible at `/docs`). - - Read more about it in the - [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/). - - And in the - [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). - """ - ), - ] = None, - callbacks: Annotated[ - Optional[List[BaseRoute]], - Doc( - """ - OpenAPI callbacks that should apply to all *path operations* in this - router. - - It will be added to the generated OpenAPI (e.g. visible at `/docs`). - - Read more about it in the - [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). - """ - ), - ] = None, - routes: Annotated[ - Optional[List[BaseRoute]], - Doc( - """ - **Note**: you probably shouldn't use this parameter, it is inherited - from Starlette and supported for compatibility. - - --- - - A list of routes to serve incoming HTTP and WebSocket requests. - """ - ), - deprecated( - """ - You normally wouldn't use this parameter with FastAPI, it is inherited - from Starlette and supported for compatibility. - - In FastAPI, you normally would use the *path operation methods*, - like `router.get()`, `router.post()`, etc. - """ - ), - ] = None, - redirect_slashes: Annotated[ - bool, - Doc( - """ - Whether to detect and redirect slashes in URLs when the client doesn't - use the same format. - """ - ), - ] = True, - default: Annotated[ - Optional["ASGIApp"], - Doc( - """ - Default function handler for this router. Used to handle - 404 Not Found errors. - """ - ), - ] = None, - dependency_overrides_provider: Annotated[ - Optional[Any], - Doc( - """ - Only used internally by FastAPI to handle dependency overrides. - - You shouldn't need to use it. It normally points to the `FastAPI` app - object. - """ - ), - ] = None, - route_class: Annotated[ - Type["APIRoute"], - Doc( - """ - Custom route (*path operation*) class to be used by this router. - - Read more about it in the - [FastAPI docs for Custom Request and APIRoute class](https://fastapi.tiangolo.com/how-to/custom-request-and-route/#custom-apiroute-class-in-a-router). - """ - ), - ] = APIRoute, - on_startup: Annotated[ - Optional[Sequence[Callable[[], Any]]], - Doc( - """ - A list of startup event handler functions. - - You should instead use the `lifespan` handlers. - - Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ - ), - ] = None, - on_shutdown: Annotated[ - Optional[Sequence[Callable[[], Any]]], - Doc( - """ - A list of shutdown event handler functions. - - You should instead use the `lifespan` handlers. - - Read more in the - [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ - ), - ] = None, - lifespan: Annotated[ - Optional["Lifespan[Any]"], - Doc( - """ - A `Lifespan` context manager handler. This replaces `startup` and - `shutdown` functions with a single context manager. - - Read more in the - [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ - ), - ] = None, - deprecated: Annotated[ - Optional[bool], - Doc( - """ - Mark all *path operations* in this router as deprecated. - - It will be added to the generated OpenAPI (e.g. visible at `/docs`). - - Read more about it in the - [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). - """ - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc( - """ - To include (or not) all the *path operations* in this router in the - generated OpenAPI. - - This affects the generated OpenAPI (e.g. visible at `/docs`). - - Read more about it in the - [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). - """ - ), - ] = True, - generate_unique_id_function: Annotated[ - Callable[["APIRoute"], str], - Doc( - """ - Customize the function used to generate unique IDs for the *path - operations* shown in the generated OpenAPI. - - This is particularly useful when automatically generating clients or - SDKs for your API. - - Read more about it in the - [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). - """ - ), - ] = Default(generate_unique_id), + prefix: str = "", + tags: list[Union[str, "Enum"]] | None = None, + dependencies: Sequence["params.Depends"] | None = None, + default_response_class: type["Response"] = Default(JSONResponse), + responses: dict[int | str, "AnyDict"] | None = None, + callbacks: list[BaseRoute] | None = None, + routes: list[BaseRoute] | None = None, + redirect_slashes: bool = True, + default: Optional["ASGIApp"] = None, + dependency_overrides_provider: Any | None = None, + route_class: type["APIRoute"] = APIRoute, + on_startup: Sequence[Callable[[], Any]] | None = None, + on_shutdown: Sequence[Callable[[], Any]] | None = None, + lifespan: Optional["Lifespan[Any]"] = None, + deprecated: bool | None = None, + include_in_schema: bool = True, + generate_unique_id_function: Callable[["APIRoute"], str] = Default( + generate_unique_id + ), ) -> None: + """Initialize the KafkaRouter. + + Args: + bootstrap_servers: A `host[:port]` string (or list of `host[:port]` strings) that the consumer should contact to bootstrap + initial cluster metadata. + + This does not have to be the full node list. + It just needs to have at least one broker that will respond to a + Metadata API Request. Default port is 9092. + request_timeout_ms: Client request timeout in milliseconds. + retry_backoff_ms: Milliseconds to backoff when retrying on errors. + metadata_max_age_ms: The period of time in milliseconds after + which we force a refresh of metadata even if we haven't seen any + partition leadership changes to proactively discover any new + brokers or partitions. + connections_max_idle_ms: Close idle connections after the number + of milliseconds specified by this config. Specifying `None` will + disable idle checks. + client_id: A name for this client. This string is passed in + each request to servers and can be used to identify specific + server-side log entries that correspond to this client. Also + submitted to :class:`~.consumer.group_coordinator.GroupCoordinator` + for logging with respect to consumer group administration. + allow_auto_create_topics: Allow automatic topic creation on the broker when subscribing to or assigning non-existent topics. + config: Extra configuration for the confluent-kafka-python + producer/consumer. See `confluent_kafka.Config `_. + acks: One of ``0``, ``1``, ``all``. The number of acknowledgments + the producer requires the leader to have received before considering a + request complete. This controls the durability of records that are + sent. The following settings are common: + + * ``0``: Producer will not wait for any acknowledgment from the server + at all. The message will immediately be added to the socket + buffer and considered sent. No guarantee can be made that the + server has received the record in this case, and the retries + configuration will not take effect (as the client won't + generally know of any failures). The offset given back for each + record will always be set to -1. + * ``1``: The broker leader will write the record to its local log but + will respond without awaiting full acknowledgement from all + followers. In this case should the leader fail immediately + after acknowledging the record but before the followers have + replicated it then the record will be lost. + * ``all``: The broker leader will wait for the full set of in-sync + replicas to acknowledge the record. This guarantees that the + record will not be lost as long as at least one in-sync replica + remains alive. This is the strongest available guarantee. + + If unset, defaults to ``acks=1``. If `enable_idempotence` is + :data:`True` defaults to ``acks=all``. + compression_type: The compression type for all data generated bythe producer. + Compression is of full batches of data, so the efficacy of batching + will also impact the compression ratio (more batching means better + compression). + partitioner: Callable used to determine which partition + each message is assigned to. Called (after key serialization): + ``partitioner(key_bytes, all_partitions, available_partitions)``. + The default partitioner implementation hashes each non-None key + using the same murmur2 algorithm as the Java client so that + messages with the same key are assigned to the same partition. + When a key is :data:`None`, the message is delivered to a random partition + (filtered to partitions with available leaders only, if possible). + max_request_size: The maximum size of a request. This is also + effectively a cap on the maximum record size. Note that the server + has its own cap on record size which may be different from this. + This setting will limit the number of record batches the producer + will send in a single request to avoid sending huge requests. + linger_ms: The producer groups together any records that arrive + in between request transmissions into a single batched request. + Normally this occurs only under load when records arrive faster + than they can be sent out. However in some circumstances the client + may want to reduce the number of requests even under moderate load. + This setting accomplishes this by adding a small amount of + artificial delay; that is, if first request is processed faster, + than `linger_ms`, producer will wait ``linger_ms - process_time``. + enable_idempotence: When set to `True`, the producer will + ensure that exactly one copy of each message is written in the + stream. If `False`, producer retries due to broker failures, + etc., may write duplicates of the retried message in the stream. + Note that enabling idempotence acks to set to ``all``. If it is not + explicitly set by the user it will be chosen. + transactional_id: Transactional ID for the producer. + transaction_timeout_ms: Transaction timeout in milliseconds. + graceful_timeout: Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down. + decoder: Custom decoder object. + parser: Custom parser object. + middlewares: Middlewares to apply to all broker publishers/subscribers. + security: Security options to connect broker and generate Specification server security information. + specification_url: Specification hardcoded server addresses. Use `servers` if not specified. + protocol: Specification server protocol. + protocol_version: Specification server protocol version. + description: Specification server description. + specification_tags: Specification server tags. + logger: User specified logger to pass into Context and log service messages. + log_level: Service messages log level. + setup_state: Whether to add broker to app scope in lifespan. + You should disable this option at old ASGI servers. + """ super().__init__( bootstrap_servers=bootstrap_servers, client_id=client_id, @@ -575,14 +258,13 @@ def __init__( # logger options logger=logger, log_level=log_level, - log_fmt=log_fmt, - # AsyncAPI options + # Specification options security=security, protocol=protocol, description=description, protocol_version=protocol_version, - asyncapi_tags=asyncapi_tags, - asyncapi_url=asyncapi_url, + specification_tags=specification_tags, + specification_url=specification_url, # FastAPI kwargs prefix=prefix, tags=tags, @@ -606,1619 +288,339 @@ def __init__( @overload # type: ignore[override] def subscriber( self, - *topics: Annotated[ - str, - Doc("Kafka topics to consume messages from."), - ], + *topics: str, partitions: Sequence["TopicPartition"] = (), polling_interval: float = 0.1, - group_id: Annotated[ - Optional[str], - Doc( - """ - Name of the consumer group to join for dynamic - partition assignment (if enabled), and to use for fetching and - committing offsets. If `None`, auto-partition assignment (via - group coordinator) and offset commits are disabled. - """ - ), - ] = None, - group_instance_id: Annotated[ - Optional[str], - Doc( - """ - A unique string that identifies the consumer instance. - If set, the consumer is treated as a static member of the group - and does not participate in consumer group management (e.g. - partition assignment, rebalances). This can be used to assign - partitions to specific consumers, rather than letting the group - assign partitions based on consumer metadata. - """ - ), - ] = None, - fetch_max_wait_ms: Annotated[ - int, - Doc( - """ - The maximum amount of time in milliseconds - the server will block before answering the fetch request if - there isn't sufficient data to immediately satisfy the - requirement given by `fetch_min_bytes`. - """ - ), - ] = 500, - fetch_max_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data the server should - return for a fetch request. This is not an absolute maximum, if - the first message in the first non-empty partition of the fetch - is larger than this value, the message will still be returned - to ensure that the consumer can make progress. NOTE: consumer - performs fetches to multiple brokers in parallel so memory - usage will depend on the number of brokers containing - partitions for the topic. - """ - ), - ] = 50 * 1024 * 1024, - fetch_min_bytes: Annotated[ - int, - Doc( - """ - Minimum amount of data the server should - return for a fetch request, otherwise wait up to - `fetch_max_wait_ms` for more data to accumulate. - """ - ), - ] = 1, - max_partition_fetch_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data - per-partition the server will return. The maximum total memory - used for a request ``= #partitions * max_partition_fetch_bytes``. - This size must be at least as large as the maximum message size - the server allows or else it is possible for the producer to - send messages larger than the consumer can fetch. If that - happens, the consumer can get stuck trying to fetch a large - message on a certain partition. - """ - ), - ] = 1 * 1024 * 1024, - auto_offset_reset: Annotated[ - Literal["latest", "earliest", "none"], - Doc( - """ - A policy for resetting offsets on `OffsetOutOfRangeError` errors: - - * `earliest` will move to the oldest available message - * `latest` will move to the most recent - * `none` will raise an exception so you can handle this case - """ - ), - ] = "latest", - auto_commit: Annotated[ - bool, - Doc( - """ - If `True` the consumer's offset will be - periodically committed in the background. - """ - ), - ] = True, - auto_commit_interval_ms: Annotated[ - int, - Doc( - """ - Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" - ), - ] = 5 * 1000, - check_crcs: Annotated[ - bool, - Doc( - """ - Automatically check the CRC32 of the records - consumed. This ensures no on-the-wire or on-disk corruption to - the messages occurred. This check adds some overhead, so it may - be disabled in cases seeking extreme performance. - """ - ), - ] = True, - partition_assignment_strategy: Annotated[ - Sequence[str], - Doc( - """ - List of objects to use to - distribute partition ownership amongst consumer instances when - group management is used. This preference is implicit in the order - of the strategies in the list. When assignment strategy changes: - to support a change to the assignment strategy, new versions must - enable support both for the old assignment strategy and the new - one. The coordinator will choose the old assignment strategy until - all members have been updated. Then it will choose the new - strategy. - """ - ), - ] = ("roundrobin",), - max_poll_interval_ms: Annotated[ - int, - Doc( - """ - Maximum allowed time between calls to - consume messages in batches. If this interval - is exceeded the consumer is considered failed and the group will - rebalance in order to reassign the partitions to another consumer - group member. If API methods block waiting for messages, that time - does not count against this timeout. - """ - ), - ] = 5 * 60 * 1000, - session_timeout_ms: Annotated[ - int, - Doc( - """ - Client group session and failure detection - timeout. The consumer sends periodic heartbeats - (`heartbeat.interval.ms`) to indicate its liveness to the broker. - If no hearts are received by the broker for a group member within - the session timeout, the broker will remove the consumer from the - group and trigger a rebalance. The allowed range is configured with - the **broker** configuration properties - `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ - ), - ] = 10 * 1000, - heartbeat_interval_ms: Annotated[ - int, - Doc( - """ - The expected time in milliseconds - between heartbeats to the consumer coordinator when using - Kafka's group management feature. Heartbeats are used to ensure - that the consumer's session stays active and to facilitate - rebalancing when new consumers join or leave the group. The - value must be set lower than `session_timeout_ms`, but typically - should be set no higher than 1/3 of that value. It can be - adjusted even lower to control the expected time for normal - rebalances. - """ - ), - ] = 3 * 1000, - isolation_level: Annotated[ - Literal["read_uncommitted", "read_committed"], - Doc( - """ - Controls how to read messages written - transactionally. - - * `read_committed`, batch consumer will only return - transactional messages which have been committed. - - * `read_uncommitted` (the default), batch consumer will - return all messages, even transactional messages which have been - aborted. - - Non-transactional messages will be returned unconditionally in - either mode. - - Messages will always be returned in offset order. Hence, in - `read_committed` mode, batch consumer will only return - messages up to the last stable offset (LSO), which is the one less - than the offset of the first open transaction. In particular any - messages appearing after messages belonging to ongoing transactions - will be withheld until the relevant transaction has been completed. - As a result, `read_committed` consumers will not be able to read up - to the high watermark when there are in flight transactions. - Further, when in `read_committed` the seek_to_end method will - return the LSO. See method docs below. - """ - ), + group_id: str | None = None, + group_instance_id: str | None = None, + fetch_max_wait_ms: int = 500, + fetch_max_bytes: int = 50 * 1024 * 1024, + fetch_min_bytes: int = 1, + max_partition_fetch_bytes: int = 1 * 1024 * 1024, + auto_offset_reset: Literal["latest", "earliest", "none"] = "latest", + auto_commit: bool = EMPTY, + auto_commit_interval_ms: int = 5 * 1000, + check_crcs: bool = True, + partition_assignment_strategy: Sequence[str] = ("roundrobin",), + max_poll_interval_ms: int = 5 * 60 * 1000, + session_timeout_ms: int = 10 * 1000, + heartbeat_interval_ms: int = 3 * 1000, + isolation_level: Literal[ + "read_uncommitted", "read_committed" ] = "read_uncommitted", - batch: Annotated[ - Literal[False], - Doc("Whether to consume messages in batches or not."), - ] = False, - max_records: Annotated[ - Optional[int], - Doc("Number of messages to consume as one batch."), - ] = None, + batch: Literal[False] = False, + max_records: int | None = None, # broker args - dependencies: Annotated[ - Iterable["params.Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **Message** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, + dependencies: Iterable["params.Depends"] = (), + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, + ] = (), + no_ack: bool = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: bool = False, + # Specification args + title: str | None = None, + description: str | None = None, + include_in_schema: bool = True, # FastAPI args - response_model: Annotated[ - Any, - Doc( - """ - The type to use for the response. - - It could be any valid Pydantic *field* type. So, it doesn't have to - be a Pydantic model, it could be other things, like a `list`, `dict`, - etc. - - It will be used for: - - * Documentation: the generated OpenAPI (and the UI at `/docs`) will - show it as the response (JSON Schema). - * Serialization: you could return an arbitrary object and the - `response_model` would be used to serialize that object into the - corresponding JSON. - * Filtering: the JSON sent to the client will only contain the data - (fields) defined in the `response_model`. If you returned an object - that contains an attribute `password` but the `response_model` does - not include that field, the JSON sent to the client would not have - that `password`. - * Validation: whatever you return will be serialized with the - `response_model`, converting any data as necessary to generate the - corresponding JSON. But if the data in the object returned is not - valid, that would mean a violation of the contract with the client, - so it's an error from the API developer. So, FastAPI will raise an - error and return a 500 error code (Internal Server Error). - - Read more about it in the - [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). - """ - ), - ] = Default(None), - response_model_include: Annotated[ - Optional["IncEx"], - Doc( - """ - Configuration passed to Pydantic to include only certain fields in the - response data. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = None, - response_model_exclude: Annotated[ - Optional["IncEx"], - Doc( - """ - Configuration passed to Pydantic to exclude certain fields in the - response data. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = None, - response_model_by_alias: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response model - should be serialized by alias when an alias is used. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = True, - response_model_exclude_unset: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data - should have all the fields, including the ones that were not set and - have their default values. This is different from - `response_model_exclude_defaults` in that if the fields are set, - they will be included in the response, even if the value is the same - as the default. - - When `True`, default values are omitted from the response. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ - ), - ] = False, - response_model_exclude_defaults: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data - should have all the fields, including the ones that have the same value - as the default. This is different from `response_model_exclude_unset` - in that if the fields are set but contain the same default values, - they will be excluded from the response. - - When `True`, default values are omitted from the response. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ - ), - ] = False, - response_model_exclude_none: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data should - exclude fields set to `None`. - - This is much simpler (less smart) than `response_model_exclude_unset` - and `response_model_exclude_defaults`. You probably want to use one of - those two instead of this one, as those allow returning `None` values - when it makes sense. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). - """ - ), - ] = False, - ) -> "AsyncAPIDefaultSubscriber": ... + response_model: Any = Default(None), + response_model_include: Optional["IncEx"] = None, + response_model_exclude: Optional["IncEx"] = None, + response_model_by_alias: bool = True, + response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, + ) -> Union[ + "DefaultSubscriber", + "ConcurrentDefaultSubscriber", + ]: ... @overload def subscriber( self, - *topics: Annotated[ - str, - Doc("Kafka topics to consume messages from."), - ], + *topics: str, partitions: Sequence["TopicPartition"] = (), polling_interval: float = 0.1, - group_id: Annotated[ - Optional[str], - Doc( - """ - Name of the consumer group to join for dynamic - partition assignment (if enabled), and to use for fetching and - committing offsets. If `None`, auto-partition assignment (via - group coordinator) and offset commits are disabled. - """ - ), - ] = None, - group_instance_id: Annotated[ - Optional[str], - Doc( - """ - A unique string that identifies the consumer instance. - If set, the consumer is treated as a static member of the group - and does not participate in consumer group management (e.g. - partition assignment, rebalances). This can be used to assign - partitions to specific consumers, rather than letting the group - assign partitions based on consumer metadata. - """ - ), - ] = None, - fetch_max_wait_ms: Annotated[ - int, - Doc( - """ - The maximum amount of time in milliseconds - the server will block before answering the fetch request if - there isn't sufficient data to immediately satisfy the - requirement given by `fetch_min_bytes`. - """ - ), - ] = 500, - fetch_max_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data the server should - return for a fetch request. This is not an absolute maximum, if - the first message in the first non-empty partition of the fetch - is larger than this value, the message will still be returned - to ensure that the consumer can make progress. NOTE: consumer - performs fetches to multiple brokers in parallel so memory - usage will depend on the number of brokers containing - partitions for the topic. - """ - ), - ] = 50 * 1024 * 1024, - fetch_min_bytes: Annotated[ - int, - Doc( - """ - Minimum amount of data the server should - return for a fetch request, otherwise wait up to - `fetch_max_wait_ms` for more data to accumulate. - """ - ), - ] = 1, - max_partition_fetch_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data - per-partition the server will return. The maximum total memory - used for a request ``= #partitions * max_partition_fetch_bytes``. - This size must be at least as large as the maximum message size - the server allows or else it is possible for the producer to - send messages larger than the consumer can fetch. If that - happens, the consumer can get stuck trying to fetch a large - message on a certain partition. - """ - ), - ] = 1 * 1024 * 1024, - auto_offset_reset: Annotated[ - Literal["latest", "earliest", "none"], - Doc( - """ - A policy for resetting offsets on `OffsetOutOfRangeError` errors: - - * `earliest` will move to the oldest available message - * `latest` will move to the most recent - * `none` will raise an exception so you can handle this case - """ - ), - ] = "latest", - auto_commit: Annotated[ - bool, - Doc( - """ - If `True` the consumer's offset will be - periodically committed in the background. - """ - ), - ] = True, - auto_commit_interval_ms: Annotated[ - int, - Doc( - """ - Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" - ), - ] = 5 * 1000, - check_crcs: Annotated[ - bool, - Doc( - """ - Automatically check the CRC32 of the records - consumed. This ensures no on-the-wire or on-disk corruption to - the messages occurred. This check adds some overhead, so it may - be disabled in cases seeking extreme performance. - """ - ), - ] = True, - partition_assignment_strategy: Annotated[ - Sequence[str], - Doc( - """ - List of objects to use to - distribute partition ownership amongst consumer instances when - group management is used. This preference is implicit in the order - of the strategies in the list. When assignment strategy changes: - to support a change to the assignment strategy, new versions must - enable support both for the old assignment strategy and the new - one. The coordinator will choose the old assignment strategy until - all members have been updated. Then it will choose the new - strategy. - """ - ), - ] = ("roundrobin",), - max_poll_interval_ms: Annotated[ - int, - Doc( - """ - Maximum allowed time between calls to - consume messages in batches. If this interval - is exceeded the consumer is considered failed and the group will - rebalance in order to reassign the partitions to another consumer - group member. If API methods block waiting for messages, that time - does not count against this timeout. - """ - ), - ] = 5 * 60 * 1000, - session_timeout_ms: Annotated[ - int, - Doc( - """ - Client group session and failure detection - timeout. The consumer sends periodic heartbeats - (`heartbeat.interval.ms`) to indicate its liveness to the broker. - If no hearts are received by the broker for a group member within - the session timeout, the broker will remove the consumer from the - group and trigger a rebalance. The allowed range is configured with - the **broker** configuration properties - `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ - ), - ] = 10 * 1000, - heartbeat_interval_ms: Annotated[ - int, - Doc( - """ - The expected time in milliseconds - between heartbeats to the consumer coordinator when using - Kafka's group management feature. Heartbeats are used to ensure - that the consumer's session stays active and to facilitate - rebalancing when new consumers join or leave the group. The - value must be set lower than `session_timeout_ms`, but typically - should be set no higher than 1/3 of that value. It can be - adjusted even lower to control the expected time for normal - rebalances. - """ - ), - ] = 3 * 1000, - isolation_level: Annotated[ - Literal["read_uncommitted", "read_committed"], - Doc( - """ - Controls how to read messages written - transactionally. - - * `read_committed`, batch consumer will only return - transactional messages which have been committed. - - * `read_uncommitted` (the default), batch consumer will - return all messages, even transactional messages which have been - aborted. - - Non-transactional messages will be returned unconditionally in - either mode. - - Messages will always be returned in offset order. Hence, in - `read_committed` mode, batch consumer will only return - messages up to the last stable offset (LSO), which is the one less - than the offset of the first open transaction. In particular any - messages appearing after messages belonging to ongoing transactions - will be withheld until the relevant transaction has been completed. - As a result, `read_committed` consumers will not be able to read up - to the high watermark when there are in flight transactions. - Further, when in `read_committed` the seek_to_end method will - return the LSO. See method docs below. - """ - ), + group_id: str | None = None, + group_instance_id: str | None = None, + fetch_max_wait_ms: int = 500, + fetch_max_bytes: int = 50 * 1024 * 1024, + fetch_min_bytes: int = 1, + max_partition_fetch_bytes: int = 1 * 1024 * 1024, + auto_offset_reset: Literal["latest", "earliest", "none"] = "latest", + auto_commit: bool = EMPTY, + auto_commit_interval_ms: int = 5 * 1000, + check_crcs: bool = True, + partition_assignment_strategy: Sequence[str] = ("roundrobin",), + max_poll_interval_ms: int = 5 * 60 * 1000, + session_timeout_ms: int = 10 * 1000, + heartbeat_interval_ms: int = 3 * 1000, + isolation_level: Literal[ + "read_uncommitted", "read_committed" ] = "read_uncommitted", - batch: Annotated[ - Literal[True], - Doc("Whether to consume messages in batches or not."), - ], - max_records: Annotated[ - Optional[int], - Doc("Number of messages to consume as one batch."), - ] = None, + batch: Literal[True], + max_records: int | None = None, # broker args - dependencies: Annotated[ - Iterable["params.Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **Message** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, + dependencies: Iterable["params.Depends"] = (), + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, + ] = (), + # Specification args + title: str | None = None, + description: str | None = None, + include_in_schema: bool = True, # FastAPI args - response_model: Annotated[ - Any, - Doc( - """ - The type to use for the response. - - It could be any valid Pydantic *field* type. So, it doesn't have to - be a Pydantic model, it could be other things, like a `list`, `dict`, - etc. - - It will be used for: - - * Documentation: the generated OpenAPI (and the UI at `/docs`) will - show it as the response (JSON Schema). - * Serialization: you could return an arbitrary object and the - `response_model` would be used to serialize that object into the - corresponding JSON. - * Filtering: the JSON sent to the client will only contain the data - (fields) defined in the `response_model`. If you returned an object - that contains an attribute `password` but the `response_model` does - not include that field, the JSON sent to the client would not have - that `password`. - * Validation: whatever you return will be serialized with the - `response_model`, converting any data as necessary to generate the - corresponding JSON. But if the data in the object returned is not - valid, that would mean a violation of the contract with the client, - so it's an error from the API developer. So, FastAPI will raise an - error and return a 500 error code (Internal Server Error). - - Read more about it in the - [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). - """ - ), - ] = Default(None), - response_model_include: Annotated[ - Optional["IncEx"], - Doc( - """ - Configuration passed to Pydantic to include only certain fields in the - response data. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = None, - response_model_exclude: Annotated[ - Optional["IncEx"], - Doc( - """ - Configuration passed to Pydantic to exclude certain fields in the - response data. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = None, - response_model_by_alias: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response model - should be serialized by alias when an alias is used. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = True, - response_model_exclude_unset: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data - should have all the fields, including the ones that were not set and - have their default values. This is different from - `response_model_exclude_defaults` in that if the fields are set, - they will be included in the response, even if the value is the same - as the default. - - When `True`, default values are omitted from the response. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ - ), - ] = False, - response_model_exclude_defaults: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data - should have all the fields, including the ones that have the same value - as the default. This is different from `response_model_exclude_unset` - in that if the fields are set but contain the same default values, - they will be excluded from the response. - - When `True`, default values are omitted from the response. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ - ), - ] = False, - response_model_exclude_none: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data should - exclude fields set to `None`. - - This is much simpler (less smart) than `response_model_exclude_unset` - and `response_model_exclude_defaults`. You probably want to use one of - those two instead of this one, as those allow returning `None` values - when it makes sense. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). - """ - ), - ] = False, - ) -> "AsyncAPIBatchSubscriber": ... + response_model: Any = Default(None), + response_model_include: Optional["IncEx"] = None, + response_model_exclude: Optional["IncEx"] = None, + response_model_by_alias: bool = True, + response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, + ) -> "SpecificationBatchSubscriber": ... @overload def subscriber( self, - *topics: Annotated[ - str, - Doc("Kafka topics to consume messages from."), - ], + *topics: str, partitions: Sequence["TopicPartition"] = (), polling_interval: float = 0.1, - group_id: Annotated[ - Optional[str], - Doc( - """ - Name of the consumer group to join for dynamic - partition assignment (if enabled), and to use for fetching and - committing offsets. If `None`, auto-partition assignment (via - group coordinator) and offset commits are disabled. - """ - ), - ] = None, - group_instance_id: Annotated[ - Optional[str], - Doc( - """ - A unique string that identifies the consumer instance. - If set, the consumer is treated as a static member of the group - and does not participate in consumer group management (e.g. - partition assignment, rebalances). This can be used to assign - partitions to specific consumers, rather than letting the group - assign partitions based on consumer metadata. - """ - ), - ] = None, - fetch_max_wait_ms: Annotated[ - int, - Doc( - """ - The maximum amount of time in milliseconds - the server will block before answering the fetch request if - there isn't sufficient data to immediately satisfy the - requirement given by `fetch_min_bytes`. - """ - ), - ] = 500, - fetch_max_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data the server should - return for a fetch request. This is not an absolute maximum, if - the first message in the first non-empty partition of the fetch - is larger than this value, the message will still be returned - to ensure that the consumer can make progress. NOTE: consumer - performs fetches to multiple brokers in parallel so memory - usage will depend on the number of brokers containing - partitions for the topic. - """ - ), - ] = 50 * 1024 * 1024, - fetch_min_bytes: Annotated[ - int, - Doc( - """ - Minimum amount of data the server should - return for a fetch request, otherwise wait up to - `fetch_max_wait_ms` for more data to accumulate. - """ - ), - ] = 1, - max_partition_fetch_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data - per-partition the server will return. The maximum total memory - used for a request ``= #partitions * max_partition_fetch_bytes``. - This size must be at least as large as the maximum message size - the server allows or else it is possible for the producer to - send messages larger than the consumer can fetch. If that - happens, the consumer can get stuck trying to fetch a large - message on a certain partition. - """ - ), - ] = 1 * 1024 * 1024, - auto_offset_reset: Annotated[ - Literal["latest", "earliest", "none"], - Doc( - """ - A policy for resetting offsets on `OffsetOutOfRangeError` errors: - - * `earliest` will move to the oldest available message - * `latest` will move to the most recent - * `none` will raise an exception so you can handle this case - """ - ), - ] = "latest", - auto_commit: Annotated[ - bool, - Doc( - """ - If `True` the consumer's offset will be - periodically committed in the background. - """ - ), - ] = True, - auto_commit_interval_ms: Annotated[ - int, - Doc( - """ - Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" - ), - ] = 5 * 1000, - check_crcs: Annotated[ - bool, - Doc( - """ - Automatically check the CRC32 of the records - consumed. This ensures no on-the-wire or on-disk corruption to - the messages occurred. This check adds some overhead, so it may - be disabled in cases seeking extreme performance. - """ - ), - ] = True, - partition_assignment_strategy: Annotated[ - Sequence[str], - Doc( - """ - List of objects to use to - distribute partition ownership amongst consumer instances when - group management is used. This preference is implicit in the order - of the strategies in the list. When assignment strategy changes: - to support a change to the assignment strategy, new versions must - enable support both for the old assignment strategy and the new - one. The coordinator will choose the old assignment strategy until - all members have been updated. Then it will choose the new - strategy. - """ - ), - ] = ("roundrobin",), - max_poll_interval_ms: Annotated[ - int, - Doc( - """ - Maximum allowed time between calls to - consume messages in batches. If this interval - is exceeded the consumer is considered failed and the group will - rebalance in order to reassign the partitions to another consumer - group member. If API methods block waiting for messages, that time - does not count against this timeout. - """ - ), - ] = 5 * 60 * 1000, - session_timeout_ms: Annotated[ - int, - Doc( - """ - Client group session and failure detection - timeout. The consumer sends periodic heartbeats - (`heartbeat.interval.ms`) to indicate its liveness to the broker. - If no hearts are received by the broker for a group member within - the session timeout, the broker will remove the consumer from the - group and trigger a rebalance. The allowed range is configured with - the **broker** configuration properties - `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ - ), - ] = 10 * 1000, - heartbeat_interval_ms: Annotated[ - int, - Doc( - """ - The expected time in milliseconds - between heartbeats to the consumer coordinator when using - Kafka's group management feature. Heartbeats are used to ensure - that the consumer's session stays active and to facilitate - rebalancing when new consumers join or leave the group. The - value must be set lower than `session_timeout_ms`, but typically - should be set no higher than 1/3 of that value. It can be - adjusted even lower to control the expected time for normal - rebalances. - """ - ), - ] = 3 * 1000, - isolation_level: Annotated[ - Literal["read_uncommitted", "read_committed"], - Doc( - """ - Controls how to read messages written - transactionally. - - * `read_committed`, batch consumer will only return - transactional messages which have been committed. - - * `read_uncommitted` (the default), batch consumer will - return all messages, even transactional messages which have been - aborted. - - Non-transactional messages will be returned unconditionally in - either mode. - - Messages will always be returned in offset order. Hence, in - `read_committed` mode, batch consumer will only return - messages up to the last stable offset (LSO), which is the one less - than the offset of the first open transaction. In particular any - messages appearing after messages belonging to ongoing transactions - will be withheld until the relevant transaction has been completed. - As a result, `read_committed` consumers will not be able to read up - to the high watermark when there are in flight transactions. - Further, when in `read_committed` the seek_to_end method will - return the LSO. See method docs below. - """ - ), + group_id: str | None = None, + group_instance_id: str | None = None, + fetch_max_wait_ms: int = 500, + fetch_max_bytes: int = 50 * 1024 * 1024, + fetch_min_bytes: int = 1, + max_partition_fetch_bytes: int = 1 * 1024 * 1024, + auto_offset_reset: Literal["latest", "earliest", "none"] = "latest", + auto_commit: bool = EMPTY, + auto_commit_interval_ms: int = 5 * 1000, + check_crcs: bool = True, + partition_assignment_strategy: Sequence[str] = ("roundrobin",), + max_poll_interval_ms: int = 5 * 60 * 1000, + session_timeout_ms: int = 10 * 1000, + heartbeat_interval_ms: int = 3 * 1000, + isolation_level: Literal[ + "read_uncommitted", "read_committed" ] = "read_uncommitted", - batch: Annotated[ - bool, - Doc("Whether to consume messages in batches or not."), - ] = False, - max_records: Annotated[ - Optional[int], - Doc("Number of messages to consume as one batch."), - ] = None, + batch: bool = False, + max_records: int | None = None, # broker args - dependencies: Annotated[ - Iterable["params.Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **Message** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, + dependencies: Iterable["params.Depends"] = (), + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = False, - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, + ] = (), + no_ack: bool = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: bool = False, + # Specification args + title: str | None = None, + description: str | None = None, + include_in_schema: bool = True, # FastAPI args - response_model: Annotated[ - Any, - Doc( - """ - The type to use for the response. - - It could be any valid Pydantic *field* type. So, it doesn't have to - be a Pydantic model, it could be other things, like a `list`, `dict`, - etc. - - It will be used for: - - * Documentation: the generated OpenAPI (and the UI at `/docs`) will - show it as the response (JSON Schema). - * Serialization: you could return an arbitrary object and the - `response_model` would be used to serialize that object into the - corresponding JSON. - * Filtering: the JSON sent to the client will only contain the data - (fields) defined in the `response_model`. If you returned an object - that contains an attribute `password` but the `response_model` does - not include that field, the JSON sent to the client would not have - that `password`. - * Validation: whatever you return will be serialized with the - `response_model`, converting any data as necessary to generate the - corresponding JSON. But if the data in the object returned is not - valid, that would mean a violation of the contract with the client, - so it's an error from the API developer. So, FastAPI will raise an - error and return a 500 error code (Internal Server Error). - - Read more about it in the - [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). - """ - ), - ] = Default(None), - response_model_include: Annotated[ - Optional["IncEx"], - Doc( - """ - Configuration passed to Pydantic to include only certain fields in the - response data. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = None, - response_model_exclude: Annotated[ - Optional["IncEx"], - Doc( - """ - Configuration passed to Pydantic to exclude certain fields in the - response data. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = None, - response_model_by_alias: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response model - should be serialized by alias when an alias is used. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = True, - response_model_exclude_unset: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data - should have all the fields, including the ones that were not set and - have their default values. This is different from - `response_model_exclude_defaults` in that if the fields are set, - they will be included in the response, even if the value is the same - as the default. - - When `True`, default values are omitted from the response. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ - ), - ] = False, - response_model_exclude_defaults: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data - should have all the fields, including the ones that have the same value - as the default. This is different from `response_model_exclude_unset` - in that if the fields are set but contain the same default values, - they will be excluded from the response. - - When `True`, default values are omitted from the response. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ - ), - ] = False, - response_model_exclude_none: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data should - exclude fields set to `None`. - - This is much simpler (less smart) than `response_model_exclude_unset` - and `response_model_exclude_defaults`. You probably want to use one of - those two instead of this one, as those allow returning `None` values - when it makes sense. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). - """ - ), - ] = False, + response_model: Any = Default(None), + response_model_include: Optional["IncEx"] = None, + response_model_exclude: Optional["IncEx"] = None, + response_model_by_alias: bool = True, + response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, ) -> Union[ - "AsyncAPIBatchSubscriber", - "AsyncAPIDefaultSubscriber", + "BatchSubscriber", "DefaultSubscriber", "ConcurrentDefaultSubscriber" ]: ... @override def subscriber( self, - *topics: Annotated[ - str, - Doc("Kafka topics to consume messages from."), - ], + *topics: str, partitions: Sequence["TopicPartition"] = (), polling_interval: float = 0.1, - group_id: Annotated[ - Optional[str], - Doc( - """ - Name of the consumer group to join for dynamic - partition assignment (if enabled), and to use for fetching and - committing offsets. If `None`, auto-partition assignment (via - group coordinator) and offset commits are disabled. - """ - ), - ] = None, - group_instance_id: Annotated[ - Optional[str], - Doc( - """ - A unique string that identifies the consumer instance. - If set, the consumer is treated as a static member of the group - and does not participate in consumer group management (e.g. - partition assignment, rebalances). This can be used to assign - partitions to specific consumers, rather than letting the group - assign partitions based on consumer metadata. - """ - ), - ] = None, - fetch_max_wait_ms: Annotated[ - int, - Doc( - """ - The maximum amount of time in milliseconds - the server will block before answering the fetch request if - there isn't sufficient data to immediately satisfy the - requirement given by `fetch_min_bytes`. - """ - ), - ] = 500, - fetch_max_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data the server should - return for a fetch request. This is not an absolute maximum, if - the first message in the first non-empty partition of the fetch - is larger than this value, the message will still be returned - to ensure that the consumer can make progress. NOTE: consumer - performs fetches to multiple brokers in parallel so memory - usage will depend on the number of brokers containing - partitions for the topic. - """ - ), - ] = 50 * 1024 * 1024, - fetch_min_bytes: Annotated[ - int, - Doc( - """ - Minimum amount of data the server should - return for a fetch request, otherwise wait up to - `fetch_max_wait_ms` for more data to accumulate. - """ - ), - ] = 1, - max_partition_fetch_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data - per-partition the server will return. The maximum total memory - used for a request ``= #partitions * max_partition_fetch_bytes``. - This size must be at least as large as the maximum message size - the server allows or else it is possible for the producer to - send messages larger than the consumer can fetch. If that - happens, the consumer can get stuck trying to fetch a large - message on a certain partition. - """ - ), - ] = 1 * 1024 * 1024, - auto_offset_reset: Annotated[ - Literal["latest", "earliest", "none"], - Doc( - """ - A policy for resetting offsets on `OffsetOutOfRangeError` errors: - - * `earliest` will move to the oldest available message - * `latest` will move to the most recent - * `none` will raise an exception so you can handle this case - """ - ), - ] = "latest", - auto_commit: Annotated[ - bool, - Doc( - """ - If `True` the consumer's offset will be - periodically committed in the background. - """ - ), - ] = True, - auto_commit_interval_ms: Annotated[ - int, - Doc( - """ - Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" - ), - ] = 5 * 1000, - check_crcs: Annotated[ - bool, - Doc( - """ - Automatically check the CRC32 of the records - consumed. This ensures no on-the-wire or on-disk corruption to - the messages occurred. This check adds some overhead, so it may - be disabled in cases seeking extreme performance. - """ - ), - ] = True, - partition_assignment_strategy: Annotated[ - Sequence[str], - Doc( - """ - List of objects to use to - distribute partition ownership amongst consumer instances when - group management is used. This preference is implicit in the order - of the strategies in the list. When assignment strategy changes: - to support a change to the assignment strategy, new versions must - enable support both for the old assignment strategy and the new - one. The coordinator will choose the old assignment strategy until - all members have been updated. Then it will choose the new - strategy. - """ - ), - ] = ("roundrobin",), - max_poll_interval_ms: Annotated[ - int, - Doc( - """ - Maximum allowed time between calls to - consume messages in batches. If this interval - is exceeded the consumer is considered failed and the group will - rebalance in order to reassign the partitions to another consumer - group member. If API methods block waiting for messages, that time - does not count against this timeout. - """ - ), - ] = 5 * 60 * 1000, - session_timeout_ms: Annotated[ - int, - Doc( - """ - Client group session and failure detection - timeout. The consumer sends periodic heartbeats - (`heartbeat.interval.ms`) to indicate its liveness to the broker. - If no hearts are received by the broker for a group member within - the session timeout, the broker will remove the consumer from the - group and trigger a rebalance. The allowed range is configured with - the **broker** configuration properties - `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ - ), - ] = 10 * 1000, - heartbeat_interval_ms: Annotated[ - int, - Doc( - """ - The expected time in milliseconds - between heartbeats to the consumer coordinator when using - Kafka's group management feature. Heartbeats are used to ensure - that the consumer's session stays active and to facilitate - rebalancing when new consumers join or leave the group. The - value must be set lower than `session_timeout_ms`, but typically - should be set no higher than 1/3 of that value. It can be - adjusted even lower to control the expected time for normal - rebalances. - """ - ), - ] = 3 * 1000, - isolation_level: Annotated[ - Literal["read_uncommitted", "read_committed"], - Doc( - """ - Controls how to read messages written - transactionally. - - * `read_committed`, batch consumer will only return - transactional messages which have been committed. - - * `read_uncommitted` (the default), batch consumer will - return all messages, even transactional messages which have been - aborted. - - Non-transactional messages will be returned unconditionally in - either mode. - - Messages will always be returned in offset order. Hence, in - `read_committed` mode, batch consumer will only return - messages up to the last stable offset (LSO), which is the one less - than the offset of the first open transaction. In particular any - messages appearing after messages belonging to ongoing transactions - will be withheld until the relevant transaction has been completed. - As a result, `read_committed` consumers will not be able to read up - to the high watermark when there are in flight transactions. - Further, when in `read_committed` the seek_to_end method will - return the LSO. See method docs below. - """ - ), + group_id: str | None = None, + group_instance_id: str | None = None, + fetch_max_wait_ms: int = 500, + fetch_max_bytes: int = 50 * 1024 * 1024, + fetch_min_bytes: int = 1, + max_partition_fetch_bytes: int = 1 * 1024 * 1024, + auto_offset_reset: Literal["latest", "earliest", "none"] = "latest", + auto_commit: bool = EMPTY, + auto_commit_interval_ms: int = 5 * 1000, + check_crcs: bool = True, + partition_assignment_strategy: Sequence[str] = ("roundrobin",), + max_poll_interval_ms: int = 5 * 60 * 1000, + session_timeout_ms: int = 10 * 1000, + heartbeat_interval_ms: int = 3 * 1000, + isolation_level: Literal[ + "read_uncommitted", "read_committed" ] = "read_uncommitted", - batch: Annotated[ - bool, - Doc("Whether to consume messages in batches or not."), - ] = False, - max_records: Annotated[ - Optional[int], - Doc("Number of messages to consume as one batch."), - ] = None, + batch: bool = False, + max_records: int | None = None, # broker args - dependencies: Annotated[ - Iterable["params.Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **Message** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, + dependencies: Iterable["params.Depends"] = (), + parser: Optional["CustomCallable"] = None, + decoder: Optional["CustomCallable"] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, + ] = (), + no_ack: bool = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: bool = False, + # Specification args + title: str | None = None, + description: str | None = None, + include_in_schema: bool = True, # FastAPI args - response_model: Annotated[ - Any, - Doc( - """ - The type to use for the response. - - It could be any valid Pydantic *field* type. So, it doesn't have to - be a Pydantic model, it could be other things, like a `list`, `dict`, - etc. - - It will be used for: - - * Documentation: the generated OpenAPI (and the UI at `/docs`) will - show it as the response (JSON Schema). - * Serialization: you could return an arbitrary object and the - `response_model` would be used to serialize that object into the - corresponding JSON. - * Filtering: the JSON sent to the client will only contain the data - (fields) defined in the `response_model`. If you returned an object - that contains an attribute `password` but the `response_model` does - not include that field, the JSON sent to the client would not have - that `password`. - * Validation: whatever you return will be serialized with the - `response_model`, converting any data as necessary to generate the - corresponding JSON. But if the data in the object returned is not - valid, that would mean a violation of the contract with the client, - so it's an error from the API developer. So, FastAPI will raise an - error and return a 500 error code (Internal Server Error). - - Read more about it in the - [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). - """ - ), - ] = Default(None), - response_model_include: Annotated[ - Optional["IncEx"], - Doc( - """ - Configuration passed to Pydantic to include only certain fields in the - response data. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = None, - response_model_exclude: Annotated[ - Optional["IncEx"], - Doc( - """ - Configuration passed to Pydantic to exclude certain fields in the - response data. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = None, - response_model_by_alias: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response model - should be serialized by alias when an alias is used. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = True, - response_model_exclude_unset: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data - should have all the fields, including the ones that were not set and - have their default values. This is different from - `response_model_exclude_defaults` in that if the fields are set, - they will be included in the response, even if the value is the same - as the default. - - When `True`, default values are omitted from the response. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ - ), - ] = False, - response_model_exclude_defaults: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data - should have all the fields, including the ones that have the same value - as the default. This is different from `response_model_exclude_unset` - in that if the fields are set but contain the same default values, - they will be excluded from the response. - - When `True`, default values are omitted from the response. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ - ), - ] = False, - response_model_exclude_none: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data should - exclude fields set to `None`. - - This is much simpler (less smart) than `response_model_exclude_unset` - and `response_model_exclude_defaults`. You probably want to use one of - those two instead of this one, as those allow returning `None` values - when it makes sense. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). - """ - ), - ] = False, - max_workers: Annotated[ - int, - Doc("Number of workers to process messages concurrently."), - ] = 1, - ) -> Union[ - "AsyncAPIBatchSubscriber", - "AsyncAPIDefaultSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", - ]: + response_model: Any = Default(None), + response_model_include: Optional["IncEx"] = None, + response_model_exclude: Optional["IncEx"] = None, + response_model_by_alias: bool = True, + response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, + max_workers: int = 1, + ) -> Union["BatchSubscriber", "DefaultSubscriber", "ConcurrentDefaultSubscriber"]: + """Create a subscriber for Kafka topics. + + Args: + *topics: Kafka topics to consume messages from. + partitions: Sequence of topic partitions to consume from. + polling_interval: Interval between polling for new messages. + group_id: Name of the consumer group to join for dynamic + partition assignment (if enabled), and to use for fetching and + committing offsets. If `None`, auto-partition assignment (via + group coordinator) and offset commits are disabled. + group_instance_id: A unique string that identifies the consumer instance. + If set, the consumer is treated as a static member of the group + and does not participate in consumer group management (e.g. + partition assignment, rebalances). This can be used to assign + partitions to specific consumers, rather than letting the group + assign partitions based on consumer metadata. + fetch_max_wait_ms: The maximum amount of time in milliseconds + the server will block before answering the fetch request if + there isn't sufficient data to immediately satisfy the + requirement given by `fetch_min_bytes`. + fetch_max_bytes: The maximum amount of data the server should + return for a fetch request. This is not an absolute maximum, if + the first message in the first non-empty partition of the fetch + is larger than this value, the message will still be returned + to ensure that the consumer can make progress. NOTE: consumer + performs fetches to multiple brokers in parallel so memory + usage will depend on the number of brokers containing + partitions for the topic. + fetch_min_bytes: Minimum amount of data the server should + return for a fetch request, otherwise wait up to + `fetch_max_wait_ms` for more data to accumulate. + max_partition_fetch_bytes: The maximum amount of data + per-partition the server will return. The maximum total memory + used for a request ``= #partitions * max_partition_fetch_bytes``. + This size must be at least as large as the maximum message size + the server allows or else it is possible for the producer to + send messages larger than the consumer can fetch. If that + happens, the consumer can get stuck trying to fetch a large + message on a certain partition. + auto_offset_reset: A policy for resetting offsets on `OffsetOutOfRangeError` errors: + + * `earliest` will move to the oldest available message + * `latest` will move to the most recent + * `none` will raise an exception so you can handle this case + auto_commit: If `True` the consumer's offset will be + periodically committed in the background. + auto_commit_interval_ms: Milliseconds between automatic + offset commits, if `auto_commit` is `True`. + check_crcs: Automatically check the CRC32 of the records + consumed. This ensures no on-the-wire or on-disk corruption to + the messages occurred. This check adds some overhead, so it may + be disabled in cases seeking extreme performance. + partition_assignment_strategy: List of objects to use to + distribute partition ownership amongst consumer instances when + group management is used. This preference is implicit in the order + of the strategies in the list. When assignment strategy changes: + to support a change to the assignment strategy, new versions must + enable support both for the old assignment strategy and the new + one. The coordinator will choose the old assignment strategy until + all members have been updated. Then it will choose the new + strategy. + max_poll_interval_ms: Maximum allowed time between calls to + consume messages in batches. If this interval + is exceeded the consumer is considered failed and the group will + rebalance in order to reassign the partitions to another consumer + group member. If API methods block waiting for messages, that time + does not count against this timeout. + session_timeout_ms: Client group session and failure detection + timeout. The consumer sends periodic heartbeats + (`heartbeat.interval.ms`) to indicate its liveness to the broker. + If no hearts are received by the broker for a group member within + the session timeout, the broker will remove the consumer from the + group and trigger a rebalance. The allowed range is configured with + the **broker** configuration properties + `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. + heartbeat_interval_ms: The expected time in milliseconds + between heartbeats to the consumer coordinator when using + Kafka's group management feature. Heartbeats are used to ensure + that the consumer's session stays active and to facilitate + rebalancing when new consumers join or leave the group. The + value must be set lower than `session_timeout_ms`, but typically + should be set no higher than 1/3 of that value. It can be + adjusted even lower to control the expected time for normal + rebalances. + isolation_level: Controls how to read messages written + transactionally. + + * `read_committed`, batch consumer will only return + transactional messages which have been committed. + + * `read_uncommitted` (the default), batch consumer will + return all messages, even transactional messages which have been + aborted. + + Non-transactional messages will be returned unconditionally in + either mode. + + Messages will always be returned in offset order. Hence, in + `read_committed` mode, batch consumer will only return + messages up to the last stable offset (LSO), which is the one less + than the offset of the first open transaction. In particular any + messages appearing after messages belonging to ongoing transactions + will be withheld until the relevant transaction has been completed. + As a result, `read_committed` consumers will not be able to read up + to the high watermark when there are in flight transactions. + Further, when in `read_committed` the seek_to_end method will + return the LSO. See method docs below. + batch: Whether to consume messages in batches or not. + max_records: Number of messages to consume as one batch. + dependencies: Dependencies list (`[Depends(),]`) to apply to the subscriber. + parser: Parser to map original **Message** object to FastStream one. + decoder: Function to decode FastStream msg bytes body to python objects. + middlewares: Subscriber middlewares to wrap incoming message processing. + no_ack: Whether to disable **FastStream** auto acknowledgement logic or not. + ack_policy: Acknowledgement policy for message processing. + no_reply: Whether to disable **FastStream** RPC and Reply To auto responses or not. + title: Specification subscriber object title. + description: Specification subscriber object description. + Uses decorated docstring as default. + include_in_schema: Whether to include operation in Specification schema or not. + response_model: The type to use for the response. + """ subscriber = super().subscriber( *topics, polling_interval=polling_interval, @@ -2246,8 +648,7 @@ def subscriber( parser=parser, decoder=decoder, middlewares=middlewares, - filter=filter, - retry=retry, + ack_policy=ack_policy, no_ack=no_ack, no_reply=no_reply, title=title, @@ -2264,314 +665,138 @@ def subscriber( ) if batch: - return cast("AsyncAPIBatchSubscriber", subscriber) - else: - if max_workers > 1: - return cast("AsyncAPIConcurrentDefaultSubscriber", subscriber) - else: - return cast("AsyncAPIDefaultSubscriber", subscriber) + return cast("BatchSubscriber", subscriber) + if max_workers > 1: + return cast("ConcurrentDefaultSubscriber", subscriber) + return cast("DefaultSubscriber", subscriber) @overload # type: ignore[override] def publisher( self, - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], + topic: str, *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Topic name to send response."), - ] = "", - batch: Annotated[ - Literal[False], - Doc("Whether to send messages in batches or not."), - ] = False, + key: bytes | Any | None = None, + partition: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + batch: Literal[False] = False, # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - ) -> "AsyncAPIDefaultPublisher": ... + ] = (), + # Specification args + title: str | None = None, + description: str | None = None, + schema: Any | None = None, + include_in_schema: bool = True, + ) -> "DefaultPublisher": ... @overload def publisher( self, - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], + topic: str, *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Topic name to send response."), - ] = "", - batch: Annotated[ - Literal[True], - Doc("Whether to send messages in batches or not."), - ], + key: bytes | Any | None = None, + partition: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + batch: Literal[True], # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - ) -> "AsyncAPIBatchPublisher": ... + ] = (), + # Specification args + title: str | None = None, + description: str | None = None, + schema: Any | None = None, + include_in_schema: bool = True, + ) -> "BatchPublisher": ... @overload def publisher( self, - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], + topic: str, *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Topic name to send response."), - ] = "", - batch: Annotated[ - bool, - Doc("Whether to send messages in batches or not."), - ] = False, + key: bytes | Any | None = None, + partition: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + batch: bool = False, # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", - ]: ... + ] = (), + # Specification args + title: str | None = None, + description: str | None = None, + schema: Any | None = None, + include_in_schema: bool = True, + ) -> Union["BatchPublisher", "DefaultPublisher"]: ... @override def publisher( self, - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], + topic: str, *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Topic name to send response."), - ] = "", - batch: Annotated[ - bool, - Doc("Whether to send messages in batches or not."), - ] = False, + key: bytes | Any | None = None, + partition: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + batch: bool = False, # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", - ]: + ] = (), + # Specification args + title: str | None = None, + description: str | None = None, + schema: Any | None = None, + include_in_schema: bool = True, + ) -> Union["BatchPublisher", "DefaultPublisher"]: + """Publish messages to a Kafka topic. + + Args: + topic: Topic where the message will be published. + key: A key to associate with the message. Can be used to + determine which partition to send the message to. If partition + is `None` (and producer's partitioner config is left as default), + then messages with the same key will be delivered to the same + partition (but if key is `None`, partition is chosen randomly). + Must be type `bytes`, or be serializable to bytes via configured + `key_serializer`. + partition: Specify a partition. If not set, the partition will be + selected using the configured `partitioner`. + headers: Message headers to store metainformation. + **content-type** and **correlation_id** will be set automatically by framework anyway. + Can be overridden by `publish.headers` if specified. + reply_to: Topic name to send response. + batch: Whether to send messages in batches or not. + middlewares: Publisher middlewares to wrap outgoing messages. + title: Specification publisher object title. + description: Specification publisher object description. + schema: Specification publishing message type. + Should be any python-native object annotation or `pydantic.BaseModel`. + include_in_schema: Whetever to include operation in Specification schema or not. + + Returns: + Union["BatchPublisher", "DefaultPublisher"]: The publisher instance. + """ return self.broker.publisher( topic=topic, key=key, @@ -2581,7 +806,7 @@ def publisher( reply_to=reply_to, # broker options middlewares=middlewares, - # AsyncAPI options + # Specification options title=title, description=description, schema=schema, diff --git a/faststream/confluent/helpers/__init__.py b/faststream/confluent/helpers/__init__.py new file mode 100644 index 0000000000..d47a3320ab --- /dev/null +++ b/faststream/confluent/helpers/__init__.py @@ -0,0 +1,10 @@ +from .admin import AdminService +from .client import AsyncConfluentConsumer, AsyncConfluentProducer +from .config import ConfluentFastConfig + +__all__ = ( + "AdminService", + "AsyncConfluentConsumer", + "AsyncConfluentProducer", + "ConfluentFastConfig", +) diff --git a/faststream/confluent/helpers/admin.py b/faststream/confluent/helpers/admin.py new file mode 100644 index 0000000000..86c71eb31f --- /dev/null +++ b/faststream/confluent/helpers/admin.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from confluent_kafka.admin import AdminClient, NewTopic + +if TYPE_CHECKING: + from .config import ConfluentFastConfig + + +@dataclass +class CreateResult: + topic: str + error: Exception | None + + +class AdminService: + def __init__(self) -> None: + self.admin_client = None + + async def connect(self, config: "ConfluentFastConfig") -> None: + if self.admin_client is None: + self.admin_client = AdminClient(config.admin_config) + + async def disconnect(self) -> None: + self.admin_client = None + + def create_topics(self, topics: list[str]) -> list[CreateResult]: + assert self.admin_client is not None + + create_result = self.admin_client.create_topics( + [ + NewTopic(topic, num_partitions=1, replication_factor=1) + for topic in topics + ], + ) + + final_results = [] + for topic, f in create_result.items(): + try: + f.result() + + except Exception as e: + if "TOPIC_ALREADY_EXISTS" not in str(e): + result = CreateResult(topic, e) + else: + result = CreateResult(topic, None) + + else: + result = CreateResult(topic, None) + + final_results.append(result) + + return final_results diff --git a/faststream/confluent/helpers/client.py b/faststream/confluent/helpers/client.py new file mode 100644 index 0000000000..7fd0b1fc73 --- /dev/null +++ b/faststream/confluent/helpers/client.py @@ -0,0 +1,390 @@ +import asyncio +import logging +from collections.abc import Callable, Sequence +from concurrent.futures import ThreadPoolExecutor +from contextlib import suppress +from time import time +from typing import ( + TYPE_CHECKING, + Any, +) + +import anyio +from confluent_kafka import Consumer, KafkaError, KafkaException, Message, Producer + +from faststream._internal.utils.functions import call_or_await, run_in_executor +from faststream.confluent.schemas import TopicPartition +from faststream.exceptions import SetupError + +from . import config as config_module + +if TYPE_CHECKING: + from typing_extensions import NotRequired, TypedDict + + from faststream._internal.basic_types import AnyDict + from faststream._internal.logger import LoggerState + + from .admin import AdminService + + class _SendKwargs(TypedDict): + value: str | bytes | None + key: str | bytes | None + headers: list[tuple[str, str | bytes]] | None + partition: NotRequired[int] + timestamp: NotRequired[int] + on_delivery: NotRequired[Callable[..., None]] + + +class AsyncConfluentProducer: + """An asynchronous Python Kafka client using the "confluent-kafka" package.""" + + def __init__( + self, + *, + logger: "LoggerState", + config: config_module.ConfluentFastConfig, + ) -> None: + self.logger_state = logger + + self.config = config.producer_config + self.producer = Producer( + self.config, + logger=self.logger_state.logger.logger, # type: ignore[call-arg] + ) + + self.__running = True + self._poll_task = asyncio.create_task(self._poll_loop()) + + async def _poll_loop(self) -> None: + while self.__running: + with suppress(Exception): + await call_or_await(self.producer.poll, 0.1) + + async def stop(self) -> None: + """Stop the Kafka producer and flush remaining messages.""" + if self.__running: + self.__running = False + if not self._poll_task.done(): + self._poll_task.cancel() + await call_or_await(self.producer.flush) + + async def flush(self) -> None: + await call_or_await(self.producer.flush) + + async def send( + self, + topic: str, + value: str | bytes | None = None, + key: str | bytes | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: list[tuple[str, str | bytes]] | None = None, + no_confirm: bool = False, + ) -> "asyncio.Future[Message | None] | Message | None": + """Sends a single message to a Kafka topic.""" + kwargs: _SendKwargs = { + "value": value, + "key": key, + "headers": headers, + } + + if partition is not None: + kwargs["partition"] = partition + + if timestamp_ms is not None: + kwargs["timestamp"] = timestamp_ms + + loop = asyncio.get_running_loop() + result_future: asyncio.Future[Message | None] = loop.create_future() + + def ack_callback(err: Any, msg: Message | None) -> None: + if err or (msg is not None and (err := msg.error())): + loop.call_soon_threadsafe( + result_future.set_exception, KafkaException(err) + ) + else: + loop.call_soon_threadsafe(result_future.set_result, msg) + + kwargs["on_delivery"] = ack_callback + + # should be sync to prevent segfault + self.producer.produce(topic, **kwargs) + + if not no_confirm: + return await result_future + return result_future + + def create_batch(self) -> "BatchBuilder": + """Creates a batch for sending multiple messages.""" + return BatchBuilder() + + async def send_batch( + self, + batch: "BatchBuilder", + topic: str, + *, + partition: int | None, + no_confirm: bool = False, + ) -> None: + """Sends a batch of messages to a Kafka topic.""" + async with anyio.create_task_group() as tg: + for msg in batch._builder: + tg.start_soon( + self.send, + topic, + msg["value"], + msg["key"], + partition, + msg["timestamp_ms"], + msg["headers"], + no_confirm, + ) + + async def ping( + self, + timeout: float | None = 5.0, + ) -> bool: + """Implement ping using `list_topics` information request.""" + if timeout is None: + timeout = -1 + + try: + cluster_metadata = await call_or_await( + self.producer.list_topics, + timeout=timeout, + ) + + return bool(cluster_metadata) + + except Exception: + return False + + +class AsyncConfluentConsumer: + """An asynchronous Python Kafka client for consuming messages using the "confluent-kafka" package.""" + + def __init__( + self, + *topics: str, + config: config_module.ConfluentFastConfig, + logger: "LoggerState", + admin_service: "AdminService", + # kwargs options + partitions: Sequence["TopicPartition"], + bootstrap_servers: str | list[str] = "localhost", + # consumer options + client_id: str | None = "confluent-kafka-consumer", + group_id: str | None = None, + group_instance_id: str | None = None, + fetch_max_wait_ms: int = 500, + fetch_max_bytes: int = 52428800, + fetch_min_bytes: int = 1, + max_partition_fetch_bytes: int = 1 * 1024 * 1024, + retry_backoff_ms: int = 100, + auto_offset_reset: str = "latest", + enable_auto_commit: bool = True, + auto_commit_interval_ms: int = 5000, + check_crcs: bool = True, + metadata_max_age_ms: int = 5 * 60 * 1000, + partition_assignment_strategy: str | list[Any] = "roundrobin", + max_poll_interval_ms: int = 300000, + session_timeout_ms: int = 10000, + heartbeat_interval_ms: int = 3000, + security_protocol: str = "PLAINTEXT", + connections_max_idle_ms: int = 540000, + isolation_level: str = "read_uncommitted", + allow_auto_create_topics: bool = True, + ) -> None: + self.admin_client = admin_service + self.logger_state = logger + + self.topics = list(topics) + self.partitions = partitions + + if not isinstance(partition_assignment_strategy, str): + partition_assignment_strategy = ",".join( + [ + x if isinstance(x, str) else x().name + for x in partition_assignment_strategy + ], + ) + + config_from_params = { + "allow.auto.create.topics": allow_auto_create_topics, + "topic.metadata.refresh.interval.ms": 1000, + "bootstrap.servers": bootstrap_servers, + "client.id": client_id, + "group.id": group_id, + "group.instance.id": group_instance_id, + "fetch.wait.max.ms": fetch_max_wait_ms, + "fetch.max.bytes": fetch_max_bytes, + "fetch.min.bytes": fetch_min_bytes, + "max.partition.fetch.bytes": max_partition_fetch_bytes, + "fetch.error.backoff.ms": retry_backoff_ms, + "auto.offset.reset": auto_offset_reset, + "enable.auto.commit": enable_auto_commit, + "auto.commit.interval.ms": auto_commit_interval_ms, + "check.crcs": check_crcs, + "metadata.max.age.ms": metadata_max_age_ms, + "partition.assignment.strategy": partition_assignment_strategy, + "max.poll.interval.ms": max_poll_interval_ms, + "session.timeout.ms": session_timeout_ms, + "heartbeat.interval.ms": heartbeat_interval_ms, + "security.protocol": security_protocol.lower(), + "connections.max.idle.ms": connections_max_idle_ms, + "isolation.level": isolation_level, + } | config.consumer_config + + self.config = config_from_params + self.consumer = Consumer(self.config, logger=self.logger_state.logger.logger) # type: ignore[call-arg] + + # A pool with single thread is used in order to execute the commands of the consumer sequentially: + # https://github.com/ag2ai/faststream/issues/1904#issuecomment-2506990895 + self._thread_pool = ThreadPoolExecutor(max_workers=1) + + @property + def topics_to_create(self) -> list[str]: + return list({*self.topics, *(p.topic for p in self.partitions)}) + + async def start(self) -> None: + """Starts the Kafka consumer and subscribes to the specified topics.""" + if self.config.get("allow.auto.create.topics", True): + topics_creation_result = await run_in_executor( + self._thread_pool, + self.admin_client.create_topics, + self.topics_to_create, + ) + + for create_result in topics_creation_result: + if create_result.error: + self.logger_state.log( + log_level=logging.WARNING, + message=f"Failed to create topic {create_result.topic}: {create_result.error}", + ) + + else: + self.logger_state.log( + log_level=logging.WARNING, + message="Auto create topics is disabled. Make sure the topics exist.", + ) + + if self.topics: + await run_in_executor( + self._thread_pool, self.consumer.subscribe, topics=self.topics + ) + + elif self.partitions: + await run_in_executor( + self._thread_pool, + self.consumer.assign, + [p.to_confluent() for p in self.partitions], + ) + + else: + msg = "You must provide either `topics` or `partitions` option." + raise SetupError(msg) + + async def commit(self, asynchronous: bool = True) -> None: + """Commits the offsets of all messages returned by the last poll operation.""" + await run_in_executor( + self._thread_pool, self.consumer.commit, asynchronous=asynchronous + ) + + async def stop(self) -> None: + """Stops the Kafka consumer and releases all resources.""" + # NOTE: If we don't explicitly call commit and then close the consumer, the confluent consumer gets stuck. + # We are doing this to avoid the issue. + enable_auto_commit = self.config.get("enable.auto.commit", True) + + try: + if enable_auto_commit: + await self.commit(asynchronous=False) + + except Exception as e: + # No offset stored issue is not a problem - https://github.com/confluentinc/confluent-kafka-python/issues/295#issuecomment-355907183 + if "No offset stored" in str(e): + pass + else: + self.logger_state.log( + log_level=logging.ERROR, + message="Consumer closing error occurred.", + exc_info=e, + ) + + # Wrap calls to async to make method cancelable by timeout + # We shouldn't read messages and close consumer concurrently + # https://github.com/airtai/faststream/issues/1904#issuecomment-2506990895 + # Now it works without lock due `ThreadPoolExecutor(max_workers=1)` + # that makes all calls to consumer sequential + await run_in_executor(self._thread_pool, self.consumer.close) + + self._thread_pool.shutdown(wait=False) + + async def getone(self, timeout: float = 0.1) -> Message | None: + """Consumes a single message from Kafka.""" + msg = await run_in_executor(self._thread_pool, self.consumer.poll, timeout) + return check_msg_error(msg) + + async def getmany( + self, + timeout: float = 0.1, + max_records: int | None = 10, + ) -> tuple[Message, ...]: + """Consumes a batch of messages from Kafka and groups them by topic and partition.""" + raw_messages: list[Message | None] = await run_in_executor( + self._thread_pool, + self.consumer.consume, # type: ignore[arg-type] + num_messages=max_records or 10, + timeout=timeout, + ) + return tuple(x for x in map(check_msg_error, raw_messages) if x is not None) + + async def seek(self, topic: str, partition: int, offset: int) -> None: + """Seeks to the specified offset in the specified topic and partition.""" + topic_partition = TopicPartition( + topic=topic, + partition=partition, + offset=offset, + ) + await run_in_executor( + self._thread_pool, self.consumer.seek, topic_partition.to_confluent() + ) + + +def check_msg_error(msg: Message | None) -> Message | None: + """Checks for errors in the consumed message.""" + if msg is None or msg.error(): + return None + + return msg + + +class BatchBuilder: + """A helper class to build a batch of messages to send to Kafka.""" + + def __init__(self) -> None: + """Initializes a new BatchBuilder instance.""" + self._builder: list[AnyDict] = [] + + def append( + self, + *, + timestamp: int | None = None, + key: str | bytes | None = None, + value: str | bytes | None = None, + headers: list[tuple[str, bytes]] | None = None, + ) -> None: + """Appends a message to the batch with optional timestamp, key, value, and headers.""" + if key is None and value is None: + raise KafkaException( + KafkaError(40, reason="Both key and value can't be None"), + ) + + self._builder.append( + { + "timestamp_ms": timestamp or round(time() * 1000), + "key": key, + "value": value, + "headers": headers or [], + }, + ) diff --git a/faststream/confluent/helpers/config.py b/faststream/confluent/helpers/config.py new file mode 100644 index 0000000000..b19905ac82 --- /dev/null +++ b/faststream/confluent/helpers/config.py @@ -0,0 +1,417 @@ +from collections.abc import Callable, Iterable +from enum import Enum +from typing import TYPE_CHECKING, Any, Literal, Optional + +from typing_extensions import TypedDict + +from faststream.__about__ import SERVICE_NAME +from faststream._internal.constants import EMPTY +from faststream.confluent.security import parse_security + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict + from faststream.security import BaseSecurity + + +class BuiltinFeatures(str, Enum): + gzip = "gzip" + snappy = "snappy" + ssl = "ssl" + sasl = "sasl" + regex = "regex" + lz4 = "lz4" + sasl_gssapi = "sasl_gssapi" + sasl_plain = "sasl_plain" + sasl_scram = "sasl_scram" + plugins = "plugins" + zstd = "zstd" + sasl_oauthbearer = "sasl_oauthbearer" + http = "http" + oidc = "oidc" + + +class Debug(str, Enum): + generic = "generic" + broker = "broker" + topic = "topic" + metadata = "metadata" + feature = "feature" + queue = "queue" + msg = "msg" + protocol = "protocol" + cgrp = "cgrp" + security = "security" + fetch = "fetch" + interceptor = "interceptor" + plugin = "plugin" + consumer = "consumer" + admin = "admin" + eos = "eos" + mock = "mock" + assignor = "assignor" + conf = "conf" + all = "all" + + +class BrokerAddressFamily(str, Enum): + any = "any" + v4 = "v4" + v6 = "v6" + + +class SecurityProtocol(str, Enum): + plaintext = "plaintext" + ssl = "ssl" + sasl_plaintext = "sasl_plaintext" + sasl_ssl = "sasl_ssl" + + +class SASLOAUTHBearerMethod(str, Enum): + default = "default" + oidc = "oidc" + + +class GroupProtocol(str, Enum): + classic = "classic" + consumer = "consumer" + + +class OffsetStoreMethod(str, Enum): + none = "none" + file = "file" + broker = "broker" + + +class IsolationLevel(str, Enum): + read_uncommitted = "read_uncommitted" + read_committed = "read_committed" + + +class CompressionCodec(str, Enum): + none = "none" + gzip = "gzip" + snappy = "snappy" + lz4 = "lz4" + zstd = "zstd" + + +class CompressionType(str, Enum): + none = "none" + gzip = "gzip" + snappy = "snappy" + lz4 = "lz4" + zstd = "zstd" + + +class ClientDNSLookup(str, Enum): + use_all_dns_ips = "use_all_dns_ips" + resolve_canonical_bootstrap_servers_only = ( + "resolve_canonical_bootstrap_servers_only" + ) + + +_SharedConfig = { + "bootstrap_servers": "bootstrap.servers", + "client_id": "client.id", + "allow_auto_create_topics": "allow.auto.create.topics", + "connections_max_idle_ms": "connections.max.idle.ms", + "metadata_max_age_ms": "metadata.max.age.ms", +} + +_ProducerConfig = _SharedConfig | { + "request_timeout_ms": "request.timeout.ms", + "compression_type": "compression.type", + "acks": "acks", + "retry_backoff_ms": "retry.backoff.ms", + "partitioner": "partitioner", + "max_request_size": "message.max.bytes", + "linger_ms": "linger.ms", + "enable_idempotence": "enable.idempotence", + "transactional_id": "transactional.id", + "transaction_timeout_ms": "transaction.timeout.ms", +} + +_ConsumerConfig = _SharedConfig + +_AdminConfig = _SharedConfig | { + "request_timeout_ms": "request.timeout.ms", + "retry_backoff_ms": "retry.backoff.ms", +} + +ConfluentConfig = TypedDict( + "ConfluentConfig", + { + "compression.codec": CompressionCodec | str, + "compression.type": CompressionType | str, + "client.dns.lookup": ClientDNSLookup | str, + "offset.store.method": OffsetStoreMethod | str, + "isolation.level": IsolationLevel | str, + "sasl.oauthbearer.method": SASLOAUTHBearerMethod | str, + "security.protocol": SecurityProtocol | str, + "broker.address.family": BrokerAddressFamily | str, + "builtin.features": BuiltinFeatures | str, + "debug": Debug | str, + "group.protocol": GroupProtocol | str, + "client.id": str, + "metadata.broker.list": str, + "bootstrap.servers": str, + "message.max.bytes": int, + "message.copy.max.bytes": int, + "receive.message.max.bytes": int, + "max.in.flight.requests.per.connection": int, + "max.in.flight": int, + "topic.metadata.refresh.interval.ms": int, + "metadata.max.age.ms": int, + "topic.metadata.refresh.fast.interval.ms": int, + "topic.metadata.refresh.fast.cnt": int, + "topic.metadata.refresh.sparse": bool, + "topic.metadata.propagation.max.ms": int, + "topic.blacklist": str, + "socket.timeout.ms": int, + "socket.blocking.max.ms": int, + "socket.send.buffer.bytes": int, + "socket.receive.buffer.bytes": int, + "socket.keepalive.enable": bool, + "socket.nagle.disable": bool, + "socket.max.fails": int, + "broker.address.ttl": int, + "socket.connection.setup.timeout.ms": int, + "connections.max.idle.ms": int, + "reconnect.backoff.jitter.ms": int, + "reconnect.backoff.ms": int, + "reconnect.backoff.max.ms": int, + "statistics.interval.ms": int, + "enabled_events": int, + "error_cb": Callable[..., Any], + "throttle_cb": Callable[..., Any], + "stats_cb": Callable[..., Any], + "log_cb": Callable[..., Any], + "log_level": int, + "log.queue": bool, + "log.thread.name": bool, + "enable.random.seed": bool, + "log.connection.close": bool, + "background_event_cb": Callable[..., Any], + "socket_cb": Callable[..., Any], + "connect_cb": Callable[..., Any], + "closesocket_cb": Callable[..., Any], + "open_cb": Callable[..., Any], + "resolve_cb": Callable[..., Any], + "opaque": str, + "default_topic_conf": str, + "internal.termination.signal": int, + "api.version.request": bool, + "api.version.request.timeout.ms": int, + "api.version.fallback.ms": int, + "broker.version.fallback": str, + "allow.auto.create.topics": bool, + "ssl.cipher.suites": str, + "ssl.curves.list": str, + "ssl.sigalgs.list": str, + "ssl.key.location": str, + "ssl.key.password": str, + "ssl.key.pem": str, + "ssl_key": str, + "ssl.certificate.location": str, + "ssl.certificate.pem": str, + "ssl_certificate": str, + "ssl.ca.location": str, + "ssl.ca.pem": str, + "ssl_ca": str, + "ssl.ca.certificate.stores": str, + "ssl.crl.location": str, + "ssl.keystore.location": str, + "ssl.keystore.password": str, + "ssl.providers": str, + "ssl.engine.location": str, + "ssl.engine.id": str, + "ssl_engine_callback_data": str, + "enable.ssl.certificate.verification": bool, + "ssl.endpoint.identification.algorithm": str, + "ssl.certificate.verify_cb": Callable[..., Any], + "sasl.mechanisms": str, + "sasl.mechanism": str, + "sasl.kerberos.service.name": str, + "sasl.kerberos.principal": str, + "sasl.kerberos.kinit.cmd": str, + "sasl.kerberos.keytab": str, + "sasl.kerberos.min.time.before.relogin": int, + "sasl.username": str, + "sasl.password": str, + "sasl.oauthbearer.config": str, + "enable.sasl.oauthbearer.unsecure.jwt": bool, + "oauth_cb": Callable[..., Any], + "sasl.oauthbearer.client.id": str, + "sasl.oauthbearer.client.secret": str, + "sasl.oauthbearer.scope": str, + "sasl.oauthbearer.extensions": str, + "sasl.oauthbearer.token.endpoint.url": str, + "plugin.library.paths": str, + "interceptors": str, + "group.id": str, + "group.instance.id": str, + "partition.assignment.strategy": str, + "session.timeout.ms": str, + "heartbeat.interval.ms": str, + "group.protocol.type": str, + "group.remote.assignor": str, + "coordinator.query.interval.ms": int, + "max.poll.interval.ms": int, + "enable.auto.commit": bool, + "auto.commit.interval.ms": int, + "enable.auto.offset.store": bool, + "queued.min.messages": int, + "queued.max.messages.kbytes": int, + "fetch.wait.max.ms": int, + "fetch.queue.backoff.ms": int, + "fetch.message.max.bytes": int, + "max.partition.fetch.bytes": int, + "fetch.max.bytes": int, + "fetch.min.bytes": int, + "fetch.error.backoff.ms": int, + "consume_cb": Callable[..., Any], + "rebalance_cb": Callable[..., Any], + "offset_commit_cb": Callable[..., Any], + "enable.partition.eof": bool, + "check.crcs": bool, + "client.rack": str, + "transactional.id": str, + "transaction.timeout.ms": int, + "enable.idempotence": bool, + "enable.gapless.guarantee": bool, + "queue.buffering.max.messages": int, + "queue.buffering.max.kbytes": int, + "queue.buffering.max.ms": float, + "linger.ms": float, + "message.send.max.retries": int, + "retries": int, + "retry.backoff.ms": int, + "retry.backoff.max.ms": int, + "queue.buffering.backpressure.threshold": int, + "batch.num.messages": int, + "batch.size": int, + "delivery.report.only.error": bool, + "dr_cb": Callable[..., Any], + "dr_msg_cb": Callable[..., Any], + "sticky.partitioning.linger.ms": int, + "on_delivery": Callable[..., Any], + }, + total=False, +) + + +class ConfluentFastConfig: + def __init__( + self, + *, + security: Optional["BaseSecurity"] = None, + config: ConfluentConfig | None = None, + # shared + bootstrap_servers: str | Iterable[str] = "localhost", + retry_backoff_ms: int = 100, + client_id: str | None = SERVICE_NAME, + allow_auto_create_topics: bool = True, + connections_max_idle_ms: int = 9 * 60 * 1000, + metadata_max_age_ms: int = 5 * 60 * 1000, + # producer + request_timeout_ms: int = 40 * 1000, + acks: Literal[0, 1, -1, "all"] = EMPTY, + compression_type: Literal["gzip", "snappy", "lz4", "zstd"] | None = None, + partitioner: str | Callable[[bytes, list[Any], list[Any]], Any] = "consistent_random", + max_request_size: int = 1024 * 1024, + linger_ms: int = 0, + enable_idempotence: bool = False, + transactional_id: str | None = None, + transaction_timeout_ms: int = 60 * 1000, + ) -> None: + self.config = parse_security(security) | (config or {}) + + shared_config = { + "bootstrap_servers": bootstrap_servers, + "client_id": client_id, + "allow_auto_create_topics": allow_auto_create_topics, + "connections_max_idle_ms": connections_max_idle_ms, + "metadata_max_age_ms": metadata_max_age_ms, + } + + # extended consumer options are passing in `broker.subscriber` method + self.raw_consumer_config = shared_config + + self.raw_producer_config = shared_config | { + "request_timeout_ms": request_timeout_ms, + "partitioner": partitioner, + "retry_backoff_ms": retry_backoff_ms, + "max_request_size": max_request_size, + "linger_ms": linger_ms, + "enable_idempotence": enable_idempotence, + "transactional_id": transactional_id, + "transaction_timeout_ms": transaction_timeout_ms, + } + + if compression_type: + self.raw_producer_config["compression_type"] = compression_type + + if acks is EMPTY or acks == "all": + self.raw_producer_config["acks"] = -1 + else: + self.raw_producer_config["acks"] = acks + + self.raw_admin_config = shared_config | { + "request_timeout_ms": request_timeout_ms, + "retry_backoff_ms": retry_backoff_ms, + } + + @property + def consumer_config(self) -> "AnyDict": + consumer_config = _to_confluent( + {_ConsumerConfig[k]: v for k, v in self.raw_consumer_config.items()} + | self.config + ) + + if "group.id" not in consumer_config: + consumer_config["group.id"] = "faststream-consumer-group" + + return consumer_config + + @property + def producer_config(self) -> "AnyDict": + return _to_confluent( + {_ProducerConfig[k]: v for k, v in self.raw_producer_config.items()} + | self.config + ) + + @property + def admin_config(self) -> "AnyDict": + return _to_confluent( + {_AdminConfig[k]: v for k, v in self.raw_admin_config.items()} | self.config + ) + + +def _to_confluent(config: "AnyDict") -> "AnyDict": + data = config.copy() + + for key, enum in ( + ("compression.codec", CompressionCodec), + ("compression.type", CompressionType), + ("client.dns.lookup", ClientDNSLookup), + ("offset.store.method", OffsetStoreMethod), + ("isolation.level", IsolationLevel), + ("sasl.oauthbearer.method", SASLOAUTHBearerMethod), + ("security.protocol", SecurityProtocol), + ("broker.address.family", BrokerAddressFamily), + ("builtin.features", BuiltinFeatures), + ("debug", Debug), + ("group.protocol", GroupProtocol), + ): + if v := data.get(key): + data[key] = enum(v).value + + bootstrap_servers = data.get("bootstrap.servers") + if ( + bootstrap_servers + and isinstance(bootstrap_servers, Iterable) + and not isinstance(bootstrap_servers, str) + ): + data["bootstrap.servers"] = ",".join(bootstrap_servers) + + return data diff --git a/faststream/confluent/message.py b/faststream/confluent/message.py index 83ee0e814b..fd187f0ad1 100644 --- a/faststream/confluent/message.py +++ b/faststream/confluent/message.py @@ -1,6 +1,6 @@ -from typing import TYPE_CHECKING, Any, Optional, Protocol, Tuple, Union +from typing import TYPE_CHECKING, Any, Protocol, Union -from faststream.broker.message import StreamMessage +from faststream.message import AckStatus, StreamMessage if TYPE_CHECKING: from confluent_kafka import Message @@ -13,9 +13,9 @@ async def commit(self) -> None: ... async def seek( self, - topic: Optional[str], - partition: Optional[int], - offset: Optional[int], + topic: str | None, + partition: int | None, + offset: int | None, ) -> None: ... @@ -27,9 +27,9 @@ async def commit(self) -> None: async def seek( self, - topic: Optional[str], - partition: Optional[int], - offset: Optional[int], + topic: str | None, + partition: int | None, + offset: int | None, ) -> None: pass @@ -41,9 +41,9 @@ class KafkaMessage( StreamMessage[ Union[ "Message", - Tuple["Message", ...], + tuple["Message", ...], ] - ] + ], ): """Represents a Kafka message in the FastStream framework. @@ -59,9 +59,12 @@ def __init__( ) -> None: super().__init__(*args, **kwargs) - self.is_manual = is_manual self.consumer = consumer + self.is_manual = is_manual + if not is_manual: + self.committed = AckStatus.ACKED + async def ack(self) -> None: """Acknowledge the Kafka message.""" if self.is_manual and not self.committed: diff --git a/faststream/confluent/opentelemetry/middleware.py b/faststream/confluent/opentelemetry/middleware.py index d8e5906dd3..0428a09aa7 100644 --- a/faststream/confluent/opentelemetry/middleware.py +++ b/faststream/confluent/opentelemetry/middleware.py @@ -1,4 +1,3 @@ -from typing import Optional from opentelemetry.metrics import Meter, MeterProvider from opentelemetry.trace import TracerProvider @@ -6,16 +5,17 @@ from faststream.confluent.opentelemetry.provider import ( telemetry_attributes_provider_factory, ) +from faststream.confluent.response import KafkaPublishCommand from faststream.opentelemetry.middleware import TelemetryMiddleware -class KafkaTelemetryMiddleware(TelemetryMiddleware): +class KafkaTelemetryMiddleware(TelemetryMiddleware[KafkaPublishCommand]): def __init__( self, *, - tracer_provider: Optional[TracerProvider] = None, - meter_provider: Optional[MeterProvider] = None, - meter: Optional[Meter] = None, + tracer_provider: TracerProvider | None = None, + meter_provider: MeterProvider | None = None, + meter: Meter | None = None, ) -> None: super().__init__( settings_provider_factory=telemetry_attributes_provider_factory, diff --git a/faststream/confluent/opentelemetry/provider.py b/faststream/confluent/opentelemetry/provider.py index c5b9c4eb26..50046d2b10 100644 --- a/faststream/confluent/opentelemetry/provider.py +++ b/faststream/confluent/opentelemetry/provider.py @@ -1,16 +1,19 @@ -from typing import TYPE_CHECKING, Sequence, Tuple, Union, cast +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union, cast from opentelemetry.semconv.trace import SpanAttributes -from faststream.broker.types import MsgType +from faststream._internal.types import MsgType from faststream.opentelemetry import TelemetrySettingsProvider from faststream.opentelemetry.consts import MESSAGING_DESTINATION_PUBLISH_NAME if TYPE_CHECKING: from confluent_kafka import Message - from faststream.broker.message import StreamMessage - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict + from faststream.confluent.response import KafkaPublishCommand + from faststream.message import StreamMessage + from faststream.response import PublishCommand class BaseConfluentTelemetrySettingsProvider(TelemetrySettingsProvider[MsgType]): @@ -19,33 +22,33 @@ class BaseConfluentTelemetrySettingsProvider(TelemetrySettingsProvider[MsgType]) def __init__(self) -> None: self.messaging_system = "kafka" - def get_publish_attrs_from_kwargs( + def get_publish_attrs_from_cmd( self, - kwargs: "AnyDict", + cmd: "KafkaPublishCommand", ) -> "AnyDict": - attrs = { + attrs: AnyDict = { SpanAttributes.MESSAGING_SYSTEM: self.messaging_system, - SpanAttributes.MESSAGING_DESTINATION_NAME: kwargs["topic"], - SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: kwargs["correlation_id"], + SpanAttributes.MESSAGING_DESTINATION_NAME: cmd.destination, + SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: cmd.correlation_id, } - if (partition := kwargs.get("partition")) is not None: - attrs[SpanAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION] = partition + if cmd.partition is not None: + attrs[SpanAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION] = cmd.partition - if (key := kwargs.get("key")) is not None: - attrs[SpanAttributes.MESSAGING_KAFKA_MESSAGE_KEY] = key + if cmd.key is not None: + attrs[SpanAttributes.MESSAGING_KAFKA_MESSAGE_KEY] = cmd.key return attrs def get_publish_destination_name( self, - kwargs: "AnyDict", + cmd: "PublishCommand", ) -> str: - return cast("str", kwargs["topic"]) + return cmd.destination class ConfluentTelemetrySettingsProvider( - BaseConfluentTelemetrySettingsProvider["Message"] + BaseConfluentTelemetrySettingsProvider["Message"], ): def get_consume_attrs_from_message( self, @@ -74,14 +77,14 @@ def get_consume_destination_name( class BatchConfluentTelemetrySettingsProvider( - BaseConfluentTelemetrySettingsProvider[Tuple["Message", ...]] + BaseConfluentTelemetrySettingsProvider[tuple["Message", ...]], ): def get_consume_attrs_from_message( self, - msg: "StreamMessage[Tuple[Message, ...]]", + msg: "StreamMessage[tuple[Message, ...]]", ) -> "AnyDict": raw_message = msg.raw_message[0] - attrs = { + return { SpanAttributes.MESSAGING_SYSTEM: self.messaging_system, SpanAttributes.MESSAGING_MESSAGE_ID: msg.message_id, SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: msg.correlation_id, @@ -93,22 +96,16 @@ def get_consume_attrs_from_message( MESSAGING_DESTINATION_PUBLISH_NAME: raw_message.topic(), } - return attrs - def get_consume_destination_name( self, - msg: "StreamMessage[Tuple[Message, ...]]", + msg: "StreamMessage[tuple[Message, ...]]", ) -> str: return cast("str", msg.raw_message[0].topic()) def telemetry_attributes_provider_factory( msg: Union["Message", Sequence["Message"], None], -) -> Union[ - ConfluentTelemetrySettingsProvider, - BatchConfluentTelemetrySettingsProvider, -]: +) -> ConfluentTelemetrySettingsProvider | BatchConfluentTelemetrySettingsProvider: if isinstance(msg, Sequence): return BatchConfluentTelemetrySettingsProvider() - else: - return ConfluentTelemetrySettingsProvider() + return ConfluentTelemetrySettingsProvider() diff --git a/faststream/confluent/parser.py b/faststream/confluent/parser.py index fff96a8f12..4c7c9cf754 100644 --- a/faststream/confluent/parser.py +++ b/faststream/confluent/parser.py @@ -1,22 +1,30 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any -from faststream.broker.message import decode_message, gen_cor_id -from faststream.confluent.message import FAKE_CONSUMER, KafkaMessage -from faststream.utils.context.repository import context +from faststream.message import decode_message + +from .message import FAKE_CONSUMER, KafkaMessage if TYPE_CHECKING: from confluent_kafka import Message - from faststream.broker.message import StreamMessage - from faststream.confluent.subscriber.usecase import LogicSubscriber - from faststream.types import DecodedMessage + from faststream._internal.basic_types import DecodedMessage + + from .message import ConsumerProtocol, StreamMessage class AsyncConfluentParser: """A class to parse Kafka messages.""" - @staticmethod + def __init__(self, is_manual: bool = False) -> None: + self.is_manual = is_manual + self._consumer: ConsumerProtocol = FAKE_CONSUMER + + def _setup(self, consumer: "ConsumerProtocol") -> None: + self._consumer = consumer + async def parse_message( + self, message: "Message", ) -> KafkaMessage: """Parses a Kafka message.""" @@ -26,27 +34,25 @@ async def parse_message( offset = message.offset() _, timestamp = message.timestamp() - handler: Optional[LogicSubscriber[Any]] = context.get_local("handler_") - return KafkaMessage( body=body, headers=headers, reply_to=headers.get("reply_to", ""), content_type=headers.get("content-type"), message_id=f"{offset}-{timestamp}", - correlation_id=headers.get("correlation_id", gen_cor_id()), + correlation_id=headers.get("correlation_id"), raw_message=message, - consumer=getattr(handler, "consumer", None) or FAKE_CONSUMER, - is_manual=getattr(handler, "is_manual", True), + consumer=self._consumer, + is_manual=self.is_manual, ) - @staticmethod async def parse_message_batch( - message: Tuple["Message", ...], + self, + message: tuple["Message", ...], ) -> KafkaMessage: """Parses a batch of messages from a Kafka consumer.""" - body: List[Any] = [] - batch_headers: List[Dict[str, str]] = [] + body: list[Any] = [] + batch_headers: list[dict[str, str]] = [] first = message[0] last = message[-1] @@ -59,8 +65,6 @@ async def parse_message_batch( _, first_timestamp = first.timestamp() - handler: Optional[LogicSubscriber[Any]] = context.get_local("handler_") - return KafkaMessage( body=body, headers=headers, @@ -68,29 +72,28 @@ async def parse_message_batch( reply_to=headers.get("reply_to", ""), content_type=headers.get("content-type"), message_id=f"{first.offset()}-{last.offset()}-{first_timestamp}", - correlation_id=headers.get("correlation_id", gen_cor_id()), + correlation_id=headers.get("correlation_id"), raw_message=message, - consumer=getattr(handler, "consumer", None) or FAKE_CONSUMER, - is_manual=getattr(handler, "is_manual", True), + consumer=self._consumer, + is_manual=self.is_manual, ) - @staticmethod async def decode_message( + self, msg: "StreamMessage[Message]", ) -> "DecodedMessage": """Decodes a message.""" return decode_message(msg) - @classmethod async def decode_message_batch( - cls, - msg: "StreamMessage[Tuple[Message, ...]]", + self, + msg: "StreamMessage[tuple[Message, ...]]", ) -> "DecodedMessage": """Decode a batch of messages.""" - return [decode_message(await cls.parse_message(m)) for m in msg.raw_message] + return [decode_message(await self.parse_message(m)) for m in msg.raw_message] def _parse_msg_headers( - headers: Sequence[Tuple[str, Union[bytes, str]]], -) -> Dict[str, str]: + headers: Sequence[tuple[str, bytes | str]], +) -> dict[str, str]: return {i: j if isinstance(j, str) else j.decode() for i, j in headers} diff --git a/faststream/confluent/prometheus/middleware.py b/faststream/confluent/prometheus/middleware.py index 2ac27dacea..6ccca549ca 100644 --- a/faststream/confluent/prometheus/middleware.py +++ b/faststream/confluent/prometheus/middleware.py @@ -1,24 +1,33 @@ -from typing import TYPE_CHECKING, Optional, Sequence +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union +from confluent_kafka import Message + +from faststream._internal.constants import EMPTY from faststream.confluent.prometheus.provider import settings_provider_factory -from faststream.prometheus.middleware import BasePrometheusMiddleware -from faststream.types import EMPTY +from faststream.confluent.response import KafkaPublishCommand +from faststream.prometheus.middleware import PrometheusMiddleware if TYPE_CHECKING: from prometheus_client import CollectorRegistry -class KafkaPrometheusMiddleware(BasePrometheusMiddleware): +class KafkaPrometheusMiddleware( + PrometheusMiddleware[ + KafkaPublishCommand, + Union[Message, Sequence[Message]], + ] +): def __init__( self, *, registry: "CollectorRegistry", app_name: str = EMPTY, metrics_prefix: str = "faststream", - received_messages_size_buckets: Optional[Sequence[float]] = None, + received_messages_size_buckets: Sequence[float] | None = None, ) -> None: super().__init__( - settings_provider_factory=settings_provider_factory, + settings_provider_factory=settings_provider_factory, # type: ignore[arg-type] registry=registry, app_name=app_name, metrics_prefix=metrics_prefix, diff --git a/faststream/confluent/prometheus/provider.py b/faststream/confluent/prometheus/provider.py index 713cfb6a1f..d77791a1e5 100644 --- a/faststream/confluent/prometheus/provider.py +++ b/faststream/confluent/prometheus/provider.py @@ -1,6 +1,7 @@ -from typing import TYPE_CHECKING, Sequence, Tuple, Union, cast +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union, cast -from faststream.broker.message import MsgType, StreamMessage +from faststream.message.message import MsgType, StreamMessage from faststream.prometheus import ( ConsumeAttrs, MetricsSettingsProvider, @@ -9,7 +10,7 @@ if TYPE_CHECKING: from confluent_kafka import Message - from faststream.types import AnyDict + from faststream.response import PublishCommand class BaseConfluentMetricsSettingsProvider(MetricsSettingsProvider[MsgType]): @@ -18,11 +19,11 @@ class BaseConfluentMetricsSettingsProvider(MetricsSettingsProvider[MsgType]): def __init__(self) -> None: self.messaging_system = "kafka" - def get_publish_destination_name_from_kwargs( + def get_publish_destination_name_from_cmd( self, - kwargs: "AnyDict", + cmd: "PublishCommand", ) -> str: - return cast("str", kwargs["topic"]) + return cmd.destination class ConfluentMetricsSettingsProvider(BaseConfluentMetricsSettingsProvider["Message"]): @@ -38,11 +39,11 @@ def get_consume_attrs_from_message( class BatchConfluentMetricsSettingsProvider( - BaseConfluentMetricsSettingsProvider[Tuple["Message", ...]] + BaseConfluentMetricsSettingsProvider[tuple["Message", ...]] ): def get_consume_attrs_from_message( self, - msg: "StreamMessage[Tuple[Message, ...]]", + msg: "StreamMessage[tuple[Message, ...]]", ) -> ConsumeAttrs: raw_message = msg.raw_message[0] return { @@ -54,11 +55,7 @@ def get_consume_attrs_from_message( def settings_provider_factory( msg: Union["Message", Sequence["Message"], None], -) -> Union[ - ConfluentMetricsSettingsProvider, - BatchConfluentMetricsSettingsProvider, -]: +) -> ConfluentMetricsSettingsProvider | BatchConfluentMetricsSettingsProvider: if isinstance(msg, Sequence): return BatchConfluentMetricsSettingsProvider() - else: - return ConfluentMetricsSettingsProvider() + return ConfluentMetricsSettingsProvider() diff --git a/faststream/confluent/publisher/asyncapi.py b/faststream/confluent/publisher/asyncapi.py deleted file mode 100644 index 30dbd2bb3f..0000000000 --- a/faststream/confluent/publisher/asyncapi.py +++ /dev/null @@ -1,204 +0,0 @@ -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Literal, - Optional, - Sequence, - Tuple, - Union, - cast, - overload, -) - -from typing_extensions import override - -from faststream.asyncapi.schema import ( - Channel, - ChannelBinding, - CorrelationId, - Message, - Operation, -) -from faststream.asyncapi.schema.bindings import kafka -from faststream.asyncapi.utils import resolve_payloads -from faststream.broker.types import MsgType -from faststream.confluent.publisher.usecase import ( - BatchPublisher, - DefaultPublisher, - LogicPublisher, -) -from faststream.exceptions import SetupError - -if TYPE_CHECKING: - from confluent_kafka import Message as ConfluentMsg - - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware - - -class AsyncAPIPublisher(LogicPublisher[MsgType]): - """A class representing a publisher.""" - - def get_name(self) -> str: - return f"{self.topic}:Publisher" - - def get_schema(self) -> Dict[str, Channel]: - payloads = self.get_payloads() - - return { - self.name: Channel( - description=self.description, - publish=Operation( - message=Message( - title=f"{self.name}:Message", - payload=resolve_payloads(payloads, "Publisher"), - correlationId=CorrelationId( - location="$message.header#/correlation_id" - ), - ), - ), - bindings=ChannelBinding(kafka=kafka.ChannelBinding(topic=self.topic)), - ) - } - - @overload # type: ignore[override] - @staticmethod - def create( - *, - batch: Literal[False], - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[ConfluentMsg]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> "AsyncAPIDefaultPublisher": ... - - @overload - @staticmethod - def create( - *, - batch: Literal[True], - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[Tuple[ConfluentMsg, ...]]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> "AsyncAPIBatchPublisher": ... - - @overload - @staticmethod - def create( - *, - batch: bool, - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Union[ - Sequence["BrokerMiddleware[Tuple[ConfluentMsg, ...]]"], - Sequence["BrokerMiddleware[ConfluentMsg]"], - ], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", - ]: ... - - @override - @staticmethod - def create( - *, - batch: bool, - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Union[ - Sequence["BrokerMiddleware[Tuple[ConfluentMsg, ...]]"], - Sequence["BrokerMiddleware[ConfluentMsg]"], - ], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", - ]: - if batch: - if key: - raise SetupError("You can't setup `key` with batch publisher") - - return AsyncAPIBatchPublisher( - topic=topic, - partition=partition, - headers=headers, - reply_to=reply_to, - broker_middlewares=cast( - "Sequence[BrokerMiddleware[Tuple[ConfluentMsg, ...]]]", - broker_middlewares, - ), - middlewares=middlewares, - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - else: - return AsyncAPIDefaultPublisher( - key=key, - # basic args - topic=topic, - partition=partition, - headers=headers, - reply_to=reply_to, - broker_middlewares=cast( - "Sequence[BrokerMiddleware[ConfluentMsg]]", broker_middlewares - ), - middlewares=middlewares, - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - -class AsyncAPIBatchPublisher( - BatchPublisher, - AsyncAPIPublisher[Tuple["ConfluentMsg", ...]], -): - pass - - -class AsyncAPIDefaultPublisher( - DefaultPublisher, - AsyncAPIPublisher["ConfluentMsg"], -): - pass diff --git a/faststream/confluent/publisher/config.py b/faststream/confluent/publisher/config.py new file mode 100644 index 0000000000..86c8e648dd --- /dev/null +++ b/faststream/confluent/publisher/config.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass, field + +from faststream._internal.configs import ( + PublisherSpecificationConfig, + PublisherUsecaseConfig, +) +from faststream.confluent.configs import KafkaBrokerConfig + + +@dataclass(kw_only=True) +class KafkaPublisherSpecificationConfig(PublisherSpecificationConfig): + topic: str + + +@dataclass(kw_only=True) +class KafkaPublisherConfig(PublisherUsecaseConfig): + _outer_config: "KafkaBrokerConfig" = field(default_factory=KafkaBrokerConfig) + + key: bytes | str | None + topic: str + partition: int | None + headers: dict[str, str] | None + reply_to: str | None diff --git a/faststream/confluent/publisher/factory.py b/faststream/confluent/publisher/factory.py new file mode 100644 index 0000000000..6113d76610 --- /dev/null +++ b/faststream/confluent/publisher/factory.py @@ -0,0 +1,81 @@ +from collections.abc import Awaitable, Callable, Sequence +from functools import wraps +from typing import TYPE_CHECKING, Any + +from faststream.exceptions import SetupError + +from .config import KafkaPublisherConfig, KafkaPublisherSpecificationConfig +from .specification import KafkaPublisherSpecification +from .usecase import BatchPublisher, DefaultPublisher + +if TYPE_CHECKING: + from faststream._internal.types import PublisherMiddleware + from faststream.confluent.configs import KafkaBrokerConfig + + +def create_publisher( + *, + autoflush: bool, + batch: bool, + key: bytes | None, + topic: str, + partition: int | None, + headers: dict[str, str] | None, + reply_to: str, + # Publisher args + config: "KafkaBrokerConfig", + middlewares: Sequence["PublisherMiddleware"], + # Specification args + schema_: Any | None, + title_: str | None, + description_: str | None, + include_in_schema: bool, +) -> BatchPublisher | DefaultPublisher: + publisher_config = KafkaPublisherConfig( + key=key, + topic=topic, + partition=partition, + headers=headers, + reply_to=reply_to, + _outer_config=config, + middlewares=middlewares, + ) + + specification = KafkaPublisherSpecification( + _outer_config=config, + specification_config=KafkaPublisherSpecificationConfig( + topic=topic, + schema_=schema_, + title_=title_, + description_=description_, + include_in_schema=include_in_schema, + ), + ) + + publisher: BatchPublisher | DefaultPublisher + if batch: + if key: + msg = "You can't setup `key` with batch publisher" + raise SetupError(msg) + + publisher = BatchPublisher(publisher_config, specification) + publish_method = "_basic_publish_batch" + + else: + publisher = DefaultPublisher(publisher_config, specification) + publish_method = "_basic_publish" + + if autoflush: + default_publish: Callable[..., Awaitable[Any | None]] = getattr( + publisher, publish_method + ) + + @wraps(default_publish) + async def autoflush_wrapper(*args: Any, **kwargs: Any) -> Any | None: + result = await default_publish(*args, **kwargs) + await publisher.flush() + return result + + setattr(publisher, publish_method, autoflush_wrapper) + + return publisher diff --git a/faststream/confluent/publisher/fake.py b/faststream/confluent/publisher/fake.py new file mode 100644 index 0000000000..d7eb5b4f05 --- /dev/null +++ b/faststream/confluent/publisher/fake.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING, Union + +from faststream._internal.endpoint.publisher.fake import FakePublisher +from faststream.confluent.response import KafkaPublishCommand + +if TYPE_CHECKING: + from faststream._internal.producer import ProducerProto + from faststream.response.response import PublishCommand + + +class KafkaFakePublisher(FakePublisher): + """Publisher Interface implementation to use as RPC or REPLY TO answer publisher.""" + + def __init__( + self, + producer: "ProducerProto", + topic: str, + ) -> None: + super().__init__(producer=producer) + self.topic = topic + + def patch_command( + self, cmd: Union["PublishCommand", "KafkaPublishCommand"] + ) -> "KafkaPublishCommand": + cmd = super().patch_command(cmd) + real_cmd = KafkaPublishCommand.from_cmd(cmd) + real_cmd.destination = self.topic + return real_cmd diff --git a/faststream/confluent/publisher/producer.py b/faststream/confluent/publisher/producer.py index 99d0a5fd4c..2e3beb670a 100644 --- a/faststream/confluent/publisher/producer.py +++ b/faststream/confluent/publisher/producer.py @@ -1,104 +1,164 @@ -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, NoReturn, Optional from typing_extensions import override -from faststream.broker.message import encode_message -from faststream.broker.publisher.proto import ProducerProto -from faststream.broker.utils import resolve_custom_func +from faststream._internal.endpoint.utils import resolve_custom_func +from faststream._internal.producer import ProducerProto from faststream.confluent.parser import AsyncConfluentParser -from faststream.exceptions import OperationForbiddenError +from faststream.exceptions import FeatureNotSupportedException +from faststream.message import encode_message + +from .state import EmptyProducerState, ProducerState, RealProducer if TYPE_CHECKING: - from faststream.broker.types import CustomCallable - from faststream.confluent.client import AsyncConfluentProducer - from faststream.types import SendableMessage + import asyncio + + from confluent_kafka import Message + from fast_depends.library.serializer import SerializerProto + + from faststream._internal.types import CustomCallable + from faststream.confluent.helpers.client import AsyncConfluentProducer + from faststream.confluent.response import KafkaPublishCommand class AsyncConfluentFastProducer(ProducerProto): """A class to represent Kafka producer.""" + def connect(self, producer: "AsyncConfluentProducer", serializer: Optional["SerializerProto"]) -> None: ... + + async def disconnect(self) -> None: ... + + def __bool__(self) -> bool: ... + + async def ping(self, timeout: float) -> bool: ... + + async def flush(self) -> None: ... + + @override + async def publish( + self, + cmd: "KafkaPublishCommand", + ) -> "asyncio.Future[Message | None] | Message | None": ... + + @override + async def publish_batch( + self, + cmd: "KafkaPublishCommand", + ) -> None: ... + + @override + async def request( + self, + cmd: "KafkaPublishCommand", + ) -> NoReturn: ... + + +class FakeConfluentFastProducer(AsyncConfluentFastProducer): + def connect(self, producer: "AsyncConfluentProducer", serializer: Optional["SerializerProto"]) -> None: + raise NotImplementedError + + async def disconnect(self) -> None: + raise NotImplementedError + + def __bool__(self) -> bool: + return False + + async def ping(self, timeout: float) -> bool: + raise NotImplementedError + + async def flush(self) -> None: + raise NotImplementedError + + @override + async def publish( + self, + cmd: "KafkaPublishCommand", + ) -> "asyncio.Future[Message | None] | Message | None": + raise NotImplementedError + + @override + async def publish_batch( + self, + cmd: "KafkaPublishCommand", + ) -> None: + raise NotImplementedError + + @override + async def request( + self, + cmd: "KafkaPublishCommand", + ) -> NoReturn: + raise NotImplementedError + + +class AsyncConfluentFastProducerImpl(ProducerProto): + """A class to represent Kafka producer.""" + def __init__( self, - producer: "AsyncConfluentProducer", parser: Optional["CustomCallable"], decoder: Optional["CustomCallable"], ) -> None: - self._producer = producer + self._producer: ProducerState = EmptyProducerState() + self.serializer: SerializerProto | None = None # NOTE: register default parser to be compatible with request - default = AsyncConfluentParser + default = AsyncConfluentParser() self._parser = resolve_custom_func(parser, default.parse_message) self._decoder = resolve_custom_func(decoder, default.decode_message) + def connect(self, producer: "AsyncConfluentProducer", serializer: Optional["SerializerProto"]) -> None: + self._producer = RealProducer(producer) + self.serializer = serializer + + async def disconnect(self) -> None: + await self._producer.stop() + self._producer = EmptyProducerState() + + def __bool__(self) -> bool: + return bool(self._producer) + + async def ping(self, timeout: float) -> bool: + return await self._producer.ping(timeout=timeout) + + async def flush(self) -> None: + await self._producer.flush() + @override - async def publish( # type: ignore[override] + async def publish( self, - message: "SendableMessage", - topic: str, - *, - key: Optional[bytes] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: str = "", - reply_to: str = "", - no_confirm: bool = False, - ) -> None: + cmd: "KafkaPublishCommand", + ) -> "asyncio.Future[Message | None] | Message | None": """Publish a message to a topic.""" - message, content_type = encode_message(message) + message, content_type = encode_message(cmd.body, serializer=self.serializer) headers_to_send = { "content-type": content_type or "", - "correlation_id": correlation_id, - **(headers or {}), + **cmd.headers_to_publish(), } - if reply_to: - headers_to_send["reply_to"] = headers_to_send.get( - "reply_to", - reply_to, - ) - - await self._producer.send( - topic=topic, + return await self._producer.producer.send( + topic=cmd.destination, value=message, - key=key, - partition=partition, - timestamp_ms=timestamp_ms, + key=cmd.key, + partition=cmd.partition, + timestamp_ms=cmd.timestamp_ms, headers=[(i, (j or "").encode()) for i, j in headers_to_send.items()], - no_confirm=no_confirm, + no_confirm=cmd.no_confirm, ) - async def stop(self) -> None: - await self._producer.stop() - - async def flush(self) -> None: - await self._producer.flush() - + @override async def publish_batch( self, - *msgs: "SendableMessage", - topic: str, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - reply_to: str = "", - correlation_id: str = "", - no_confirm: bool = False, + cmd: "KafkaPublishCommand", ) -> None: """Publish a batch of messages to a topic.""" - batch = self._producer.create_batch() + batch = self._producer.producer.create_batch() - headers_to_send = {"correlation_id": correlation_id, **(headers or {})} + headers_to_send = cmd.headers_to_publish() - if reply_to: - headers_to_send["reply_to"] = headers_to_send.get( - "reply_to", - reply_to, - ) - - for msg in msgs: - message, content_type = encode_message(msg) + for msg in cmd.batch_bodies: + message, content_type = encode_message(msg, serializer=self.serializer) if content_type: final_headers = { @@ -111,19 +171,21 @@ async def publish_batch( batch.append( key=None, value=message, - timestamp=timestamp_ms, + timestamp=cmd.timestamp_ms, headers=[(i, j.encode()) for i, j in final_headers.items()], ) - await self._producer.send_batch( + await self._producer.producer.send_batch( batch, - topic, - partition=partition, - no_confirm=no_confirm, + cmd.destination, + partition=cmd.partition, + no_confirm=cmd.no_confirm, ) @override - async def request(self, *args: Any, **kwargs: Any) -> Optional[Any]: - raise OperationForbiddenError( - "Kafka doesn't support `request` method without test client." - ) + async def request( # type: ignore[override] + self, + cmd: "KafkaPublishCommand", + ) -> NoReturn: + msg = "Kafka doesn't support `request` method without test client." + raise FeatureNotSupportedException(msg) diff --git a/faststream/confluent/publisher/specification.py b/faststream/confluent/publisher/specification.py new file mode 100644 index 0000000000..56d7f86bc9 --- /dev/null +++ b/faststream/confluent/publisher/specification.py @@ -0,0 +1,45 @@ +from faststream._internal.endpoint.publisher import PublisherSpecification +from faststream.confluent.configs import KafkaBrokerConfig +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 .config import KafkaPublisherSpecificationConfig + + +class KafkaPublisherSpecification( + PublisherSpecification[KafkaBrokerConfig, KafkaPublisherSpecificationConfig] +): + @property + def topic(self) -> str: + return f"{self._outer_config.prefix}{self.config.topic}" + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{self.topic}:Publisher" + + def get_schema(self) -> dict[str, PublisherSpec]: + payloads = self.get_payloads() + + return { + self.name: PublisherSpec( + description=self.config.description_, + operation=Operation( + message=Message( + title=f"{self.name}:Message", + payload=resolve_payloads(payloads, "Publisher"), + ), + bindings=None, + ), + bindings=ChannelBinding( + kafka=kafka.ChannelBinding( + topic=self.topic, + partitions=None, + replicas=None, + ) + ), + ), + } diff --git a/faststream/confluent/publisher/state.py b/faststream/confluent/publisher/state.py new file mode 100644 index 0000000000..2b061a8fb2 --- /dev/null +++ b/faststream/confluent/publisher/state.py @@ -0,0 +1,58 @@ +from typing import TYPE_CHECKING, Protocol + +from faststream.exceptions import IncorrectState + +if TYPE_CHECKING: + from faststream.confluent.helpers.client import AsyncConfluentProducer + + +class ProducerState(Protocol): + producer: "AsyncConfluentProducer" + + def __bool__(self) -> bool: ... + + async def ping(self, timeout: float) -> bool: ... + + async def stop(self) -> None: ... + + async def flush(self) -> None: ... + + +class EmptyProducerState(ProducerState): + __slots__ = () + + @property + def producer(self) -> "AsyncConfluentProducer": + msg = "You can't use producer here, please connect broker first." + raise IncorrectState(msg) + + async def ping(self, timeout: float) -> bool: + return False + + def __bool__(self) -> bool: + return False + + async def stop(self) -> None: + pass + + async def flush(self) -> None: + pass + + +class RealProducer(ProducerState): + __slots__ = ("producer",) + + def __init__(self, producer: "AsyncConfluentProducer") -> None: + self.producer = producer + + def __bool__(self) -> bool: + return True + + async def stop(self) -> None: + await self.producer.stop() + + async def ping(self, timeout: float) -> bool: + return await self.producer.ping(timeout=timeout) + + async def flush(self) -> None: + await self.producer.flush() diff --git a/faststream/confluent/publisher/usecase.py b/faststream/confluent/publisher/usecase.py index 4ed68aa685..78d4624747 100644 --- a/faststream/confluent/publisher/usecase.py +++ b/faststream/confluent/publisher/usecase.py @@ -1,79 +1,48 @@ -from contextlib import AsyncExitStack -from functools import partial -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Dict, - Iterable, - Optional, - Sequence, - Tuple, - Union, - cast, -) +from collections.abc import Iterable +from typing import TYPE_CHECKING, Union from confluent_kafka import Message from typing_extensions import override -from faststream.broker.message import SourceType, gen_cor_id -from faststream.broker.publisher.usecase import PublisherUsecase -from faststream.broker.types import MsgType -from faststream.exceptions import NOT_CONNECTED_YET -from faststream.utils.functions import return_input +from faststream._internal.endpoint.publisher import PublisherUsecase +from faststream._internal.types import MsgType +from faststream.confluent.response import KafkaPublishCommand +from faststream.message import gen_cor_id +from faststream.response.publish_type import PublishType if TYPE_CHECKING: - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware + import asyncio + + from faststream._internal.basic_types import SendableMessage + from faststream._internal.types import PublisherMiddleware from faststream.confluent.message import KafkaMessage - from faststream.confluent.publisher.producer import AsyncConfluentFastProducer - from faststream.types import AnyDict, AsyncFunc, SendableMessage + from faststream.response.response import PublishCommand + + from .config import KafkaPublisherConfig + from .producer import AsyncConfluentFastProducer + from .specification import KafkaPublisherSpecification class LogicPublisher(PublisherUsecase[MsgType]): """A class to publish messages to a Kafka topic.""" - _producer: Optional["AsyncConfluentFastProducer"] + _producer: "AsyncConfluentFastProducer" def __init__( self, - *, - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: Optional[str], - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[MsgType]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, + config: "KafkaPublisherConfig", + specifcication: "KafkaPublisherSpecification", ) -> 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 - self.partition = partition - self.reply_to = reply_to - self.headers = headers + super().__init__(config, specifcication) - self._producer = None + self._topic = config.topic + self.partition = config.partition + self.reply_to = config.reply_to + self.headers = config.headers or {} - def __hash__(self) -> int: - return hash(self.topic) - - def add_prefix(self, prefix: str) -> None: - self.topic = "".join((prefix, self.topic)) + @property + def topic(self) -> str: + return f"{self._outer_config.prefix}{self._topic}" @override async def request( @@ -81,94 +50,41 @@ async def request( message: "SendableMessage", topic: str = "", *, - key: Optional[bytes] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, + key: bytes | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, timeout: float = 0.5, - # publisher specific - _extra_middlewares: Iterable["PublisherMiddleware"] = (), ) -> "KafkaMessage": - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - kwargs: AnyDict = { - "key": key, - # basic args - "timeout": timeout, - "timestamp_ms": timestamp_ms, - "topic": topic or self.topic, - "partition": partition or self.partition, - "headers": headers or self.headers, - "correlation_id": correlation_id or gen_cor_id(), - } - - request: AsyncFunc = self._producer.request - - for pub_m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - request = partial(pub_m, request) - - published_msg = await request(message, **kwargs) - - async with AsyncExitStack() as stack: - return_msg: Callable[[KafkaMessage], Awaitable[KafkaMessage]] = return_input - for m in self._broker_middlewares[::-1]: - mid = m(published_msg) - await stack.enter_async_context(mid) - return_msg = partial(mid.consume_scope, return_msg) - - parsed_msg = await self._producer._parser(published_msg) - parsed_msg._decoded_body = await self._producer._decoder(parsed_msg) - parsed_msg._source_type = SourceType.Response - return await return_msg(parsed_msg) + cmd = KafkaPublishCommand( + message, + topic=topic or self.topic, + key=key, + partition=partition or self.partition, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), + timestamp_ms=timestamp_ms, + timeout=timeout, + _publish_type=PublishType.REQUEST, + ) - raise AssertionError("unreachable") + msg: KafkaMessage = await self._basic_request(cmd) + return msg async def flush(self) -> None: - assert self._producer, NOT_CONNECTED_YET # nosec B101 await self._producer.flush() class DefaultPublisher(LogicPublisher[Message]): def __init__( self, - *, - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: Optional[str], - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[Message]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, + config: "KafkaPublisherConfig", + specifcication: "KafkaPublisherSpecification", ) -> None: - super().__init__( - topic=topic, - partition=partition, - headers=headers, - reply_to=reply_to, - # publisher args - broker_middlewares=broker_middlewares, - middlewares=middlewares, - # AsyncAPI args - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + super().__init__(config, specifcication) - self.key = key + self.key = config.key @override async def publish( @@ -176,42 +92,46 @@ async def publish( message: "SendableMessage", topic: str = "", *, - key: Optional[bytes] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, + key: bytes | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, reply_to: str = "", no_confirm: bool = False, - # publisher specific - _extra_middlewares: Iterable["PublisherMiddleware"] = (), - ) -> Optional[Any]: - assert self._producer, NOT_CONNECTED_YET # nosec B101 + ) -> "asyncio.Future": + cmd = KafkaPublishCommand( + message, + topic=topic or self.topic, + key=key or self.key, + partition=partition or self.partition, + reply_to=reply_to or self.reply_to, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), + timestamp_ms=timestamp_ms, + no_confirm=no_confirm, + _publish_type=PublishType.PUBLISH, + ) + return await self._basic_publish(cmd, _extra_middlewares=()) - kwargs: AnyDict = { - "key": key or self.key, - # basic args - "no_confirm": no_confirm, - "topic": topic or self.topic, - "partition": partition or self.partition, - "timestamp_ms": timestamp_ms, - "headers": headers or self.headers, - "reply_to": reply_to or self.reply_to, - "correlation_id": correlation_id or gen_cor_id(), - } + @override + async def _publish( + self, + cmd: Union["PublishCommand", "KafkaPublishCommand"], + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> None: + """This method should be called in subscriber flow only.""" + cmd = KafkaPublishCommand.from_cmd(cmd) - call: AsyncFunc = self._producer.publish + cmd.destination = self.topic + cmd.add_headers(self.headers, override=False) + cmd.reply_to = cmd.reply_to or self.reply_to - for m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - call = partial(m, call) + cmd.partition = cmd.partition or self.partition + cmd.key = cmd.key or self.key - return await call(message, **kwargs) + await self._basic_publish(cmd, _extra_middlewares=_extra_middlewares) @override async def request( @@ -219,17 +139,15 @@ async def request( message: "SendableMessage", topic: str = "", *, - key: Optional[bytes] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, + key: bytes | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, timeout: float = 0.5, - # publisher specific - _extra_middlewares: Iterable["PublisherMiddleware"] = (), ) -> "KafkaMessage": return await super().request( - message=message, + message, topic=topic, key=key or self.key, partition=partition, @@ -237,53 +155,51 @@ async def request( headers=headers, correlation_id=correlation_id, timeout=timeout, - _extra_middlewares=_extra_middlewares, ) -class BatchPublisher(LogicPublisher[Tuple[Message, ...]]): +class BatchPublisher(LogicPublisher[tuple[Message, ...]]): @override async def publish( self, - message: Union["SendableMessage", Iterable["SendableMessage"]], - *extra_messages: "SendableMessage", + *messages: "SendableMessage", topic: str = "", - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, reply_to: str = "", no_confirm: bool = False, - # publisher specific - _extra_middlewares: Iterable["PublisherMiddleware"] = (), ) -> None: - assert self._producer, NOT_CONNECTED_YET # nosec B101 + cmd = KafkaPublishCommand( + *messages, + key=None, + topic=topic or self.topic, + partition=partition or self.partition, + reply_to=reply_to or self.reply_to, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), + timestamp_ms=timestamp_ms, + no_confirm=no_confirm, + _publish_type=PublishType.PUBLISH, + ) - msgs: Iterable[SendableMessage] - if extra_messages: - msgs = (cast("SendableMessage", message), *extra_messages) - else: - msgs = cast("Iterable[SendableMessage]", message) + return await self._basic_publish_batch(cmd, _extra_middlewares=()) - kwargs: AnyDict = { - "topic": topic or self.topic, - "no_confirm": no_confirm, - "partition": partition or self.partition, - "timestamp_ms": timestamp_ms, - "headers": headers or self.headers, - "reply_to": reply_to or self.reply_to, - "correlation_id": correlation_id or gen_cor_id(), - } + @override + async def _publish( + self, + cmd: Union["PublishCommand", "KafkaPublishCommand"], + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> None: + """This method should be called in subscriber flow only.""" + cmd = KafkaPublishCommand.from_cmd(cmd, batch=True) - call: AsyncFunc = self._producer.publish_batch + cmd.destination = self.topic + cmd.add_headers(self.headers, override=False) + cmd.reply_to = cmd.reply_to or self.reply_to - for m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - call = partial(m, call) + cmd.partition = cmd.partition or self.partition - await call(*msgs, **kwargs) + await self._basic_publish_batch(cmd, _extra_middlewares=_extra_middlewares) diff --git a/faststream/confluent/response.py b/faststream/confluent/response.py index da420aa286..5b8ecf8e1c 100644 --- a/faststream/confluent/response.py +++ b/faststream/confluent/response.py @@ -1,11 +1,12 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union from typing_extensions import override -from faststream.broker.response import Response +from faststream.response.publish_type import PublishType +from faststream.response.response import BatchPublishCommand, PublishCommand, Response if TYPE_CHECKING: - from faststream.types import AnyDict, SendableMessage + from faststream._internal.basic_types import AnyDict, SendableMessage class KafkaResponse(Response): @@ -14,9 +15,9 @@ def __init__( body: "SendableMessage", *, headers: Optional["AnyDict"] = None, - correlation_id: Optional[str] = None, - timestamp_ms: Optional[int] = None, - key: Optional[bytes] = None, + correlation_id: str | None = None, + timestamp_ms: int | None = None, + key: bytes | None = None, ) -> None: super().__init__( body=body, @@ -28,10 +29,84 @@ def __init__( self.key = key @override - def as_publish_kwargs(self) -> "AnyDict": - publish_options = { - **super().as_publish_kwargs(), - "timestamp_ms": self.timestamp_ms, - "key": self.key, - } - return publish_options + def as_publish_command(self) -> "KafkaPublishCommand": + return KafkaPublishCommand( + self.body, + headers=self.headers, + correlation_id=self.correlation_id, + _publish_type=PublishType.PUBLISH, + # Kafka specific + topic="", + key=self.key, + timestamp_ms=self.timestamp_ms, + ) + + +class KafkaPublishCommand(BatchPublishCommand): + def __init__( + self, + message: "SendableMessage", + /, + *messages: "SendableMessage", + topic: str, + _publish_type: PublishType, + key: bytes | str | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + reply_to: str = "", + no_confirm: bool = False, + timeout: float = 0.5, + ) -> None: + super().__init__( + message, + *messages, + destination=topic, + reply_to=reply_to, + correlation_id=correlation_id, + headers=headers, + _publish_type=_publish_type, + ) + + self.key = key + self.partition = partition + self.timestamp_ms = timestamp_ms + self.no_confirm = no_confirm + + # request option + self.timeout = timeout + + @classmethod + def from_cmd( + cls, + cmd: Union["PublishCommand", "KafkaPublishCommand"], + *, + batch: bool = False, + ) -> "KafkaPublishCommand": + if isinstance(cmd, KafkaPublishCommand): + # NOTE: Should return a copy probably. + return cmd + + body, extra_bodies = cls._parse_bodies(cmd.body, batch=batch) + + return cls( + body, + *extra_bodies, + topic=cmd.destination, + correlation_id=cmd.correlation_id, + headers=cmd.headers, + reply_to=cmd.reply_to, + _publish_type=cmd.publish_type, + ) + + def headers_to_publish(self) -> dict[str, str]: + headers = {} + + if self.correlation_id: + headers["correlation_id"] = self.correlation_id + + if self.reply_to: + headers["reply_to"] = self.reply_to + + return headers | self.headers diff --git a/faststream/confluent/router.py b/faststream/confluent/router.py deleted file mode 100644 index 14dcb9b943..0000000000 --- a/faststream/confluent/router.py +++ /dev/null @@ -1,530 +0,0 @@ -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Dict, - Iterable, - Literal, - Optional, - Sequence, - Tuple, - Union, -) - -from typing_extensions import Annotated, Doc, deprecated - -from faststream.broker.router import ArgsContainer, BrokerRouter, SubscriberRoute -from faststream.broker.utils import default_filter -from faststream.confluent.broker.registrator import KafkaRegistrator - -if TYPE_CHECKING: - from confluent_kafka import Message - from fast_depends.dependencies import Depends - - from faststream.broker.types import ( - BrokerMiddleware, - CustomCallable, - Filter, - PublisherMiddleware, - SubscriberMiddleware, - ) - from faststream.confluent.message import KafkaMessage - from faststream.confluent.schemas import TopicPartition - from faststream.types import SendableMessage - - -class KafkaPublisher(ArgsContainer): - """Delayed KafkaPublisher registration object. - - Just a copy of `KafkaRegistrator.publisher(...)` arguments. - """ - - def __init__( - self, - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], - *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Topic name to send response."), - ] = "", - batch: Annotated[ - bool, - Doc("Whether to send messages in batches or not."), - ] = False, - # basic args - middlewares: Annotated[ - Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - ) -> None: - super().__init__( - topic=topic, - key=key, - partition=partition, - batch=batch, - headers=headers, - reply_to=reply_to, - # basic args - middlewares=middlewares, - # AsyncAPI args - title=title, - description=description, - schema=schema, - include_in_schema=include_in_schema, - ) - - -class KafkaRoute(SubscriberRoute): - """Class to store delaied KafkaBroker subscriber registration.""" - - def __init__( - self, - call: Annotated[ - Union[ - Callable[..., "SendableMessage"], - Callable[..., Awaitable["SendableMessage"]], - ], - Doc("Message handler function."), - ], - *topics: Annotated[ - str, - Doc("Kafka topics to consume messages from."), - ], - publishers: Annotated[ - Iterable[KafkaPublisher], - Doc("Kafka publishers to broadcast the handler result."), - ] = (), - partitions: Sequence["TopicPartition"] = (), - polling_interval: float = 0.1, - group_id: Annotated[ - Optional[str], - Doc( - """ - Name of the consumer group to join for dynamic - partition assignment (if enabled), and to use for fetching and - committing offsets. If `None`, auto-partition assignment (via - group coordinator) and offset commits are disabled. - """ - ), - ] = None, - group_instance_id: Annotated[ - Optional[str], - Doc( - """ - A unique string that identifies the consumer instance. - If set, the consumer is treated as a static member of the group - and does not participate in consumer group management (e.g. - partition assignment, rebalances). This can be used to assign - partitions to specific consumers, rather than letting the group - assign partitions based on consumer metadata. - """ - ), - ] = None, - fetch_max_wait_ms: Annotated[ - int, - Doc( - """ - The maximum amount of time in milliseconds - the server will block before answering the fetch request if - there isn't sufficient data to immediately satisfy the - requirement given by `fetch_min_bytes`. - """ - ), - ] = 500, - fetch_max_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data the server should - return for a fetch request. This is not an absolute maximum, if - the first message in the first non-empty partition of the fetch - is larger than this value, the message will still be returned - to ensure that the consumer can make progress. NOTE: consumer - performs fetches to multiple brokers in parallel so memory - usage will depend on the number of brokers containing - partitions for the topic. - """ - ), - ] = 50 * 1024 * 1024, - fetch_min_bytes: Annotated[ - int, - Doc( - """ - Minimum amount of data the server should - return for a fetch request, otherwise wait up to - `fetch_max_wait_ms` for more data to accumulate. - """ - ), - ] = 1, - max_partition_fetch_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data - per-partition the server will return. The maximum total memory - used for a request ``= #partitions * max_partition_fetch_bytes``. - This size must be at least as large as the maximum message size - the server allows or else it is possible for the producer to - send messages larger than the consumer can fetch. If that - happens, the consumer can get stuck trying to fetch a large - message on a certain partition. - """ - ), - ] = 1 * 1024 * 1024, - auto_offset_reset: Annotated[ - Literal["latest", "earliest", "none"], - Doc( - """ - A policy for resetting offsets on `OffsetOutOfRangeError` errors: - - * `earliest` will move to the oldest available message - * `latest` will move to the most recent - * `none` will raise an exception so you can handle this case - """ - ), - ] = "latest", - auto_commit: Annotated[ - bool, - Doc( - """ - If `True` the consumer's offset will be - periodically committed in the background. - """ - ), - ] = True, - auto_commit_interval_ms: Annotated[ - int, - Doc( - """ - Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" - ), - ] = 5 * 1000, - check_crcs: Annotated[ - bool, - Doc( - """ - Automatically check the CRC32 of the records - consumed. This ensures no on-the-wire or on-disk corruption to - the messages occurred. This check adds some overhead, so it may - be disabled in cases seeking extreme performance. - """ - ), - ] = True, - partition_assignment_strategy: Annotated[ - Sequence[str], - Doc( - """ - List of objects to use to - distribute partition ownership amongst consumer instances when - group management is used. This preference is implicit in the order - of the strategies in the list. When assignment strategy changes: - to support a change to the assignment strategy, new versions must - enable support both for the old assignment strategy and the new - one. The coordinator will choose the old assignment strategy until - all members have been updated. Then it will choose the new - strategy. - """ - ), - ] = ("roundrobin",), - max_poll_interval_ms: Annotated[ - int, - Doc( - """ - Maximum allowed time between calls to - consume messages in batches. If this interval - is exceeded the consumer is considered failed and the group will - rebalance in order to reassign the partitions to another consumer - group member. If API methods block waiting for messages, that time - does not count against this timeout. - """ - ), - ] = 5 * 60 * 1000, - session_timeout_ms: Annotated[ - int, - Doc( - """ - Client group session and failure detection - timeout. The consumer sends periodic heartbeats - (`heartbeat.interval.ms`) to indicate its liveness to the broker. - If no hearts are received by the broker for a group member within - the session timeout, the broker will remove the consumer from the - group and trigger a rebalance. The allowed range is configured with - the **broker** configuration properties - `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ - ), - ] = 10 * 1000, - heartbeat_interval_ms: Annotated[ - int, - Doc( - """ - The expected time in milliseconds - between heartbeats to the consumer coordinator when using - Kafka's group management feature. Heartbeats are used to ensure - that the consumer's session stays active and to facilitate - rebalancing when new consumers join or leave the group. The - value must be set lower than `session_timeout_ms`, but typically - should be set no higher than 1/3 of that value. It can be - adjusted even lower to control the expected time for normal - rebalances. - """ - ), - ] = 3 * 1000, - isolation_level: Annotated[ - Literal["read_uncommitted", "read_committed"], - Doc( - """ - Controls how to read messages written - transactionally. - - * `read_committed`, batch consumer will only return - transactional messages which have been committed. - - * `read_uncommitted` (the default), batch consumer will - return all messages, even transactional messages which have been - aborted. - - Non-transactional messages will be returned unconditionally in - either mode. - - Messages will always be returned in offset order. Hence, in - `read_committed` mode, batch consumer will only return - messages up to the last stable offset (LSO), which is the one less - than the offset of the first open transaction. In particular any - messages appearing after messages belonging to ongoing transactions - will be withheld until the relevant transaction has been completed. - As a result, `read_committed` consumers will not be able to read up - to the high watermark when there are in flight transactions. - Further, when in `read_committed` the seek_to_end method will - return the LSO. See method docs below. - """ - ), - ] = "read_uncommitted", - batch: Annotated[ - bool, - Doc("Whether to consume messages in batches or not."), - ] = False, - max_records: Annotated[ - Optional[int], - Doc("Number of messages to consume as one batch."), - ] = None, - # broker args - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **Message** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - middlewares: Annotated[ - Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), - deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - max_workers: Annotated[ - int, - Doc("Number of workers to process messages concurrently."), - ] = 1, - ) -> None: - super().__init__( - call, - *topics, - publishers=publishers, - max_workers=max_workers, - partitions=partitions, - polling_interval=polling_interval, - group_id=group_id, - group_instance_id=group_instance_id, - fetch_max_wait_ms=fetch_max_wait_ms, - fetch_max_bytes=fetch_max_bytes, - fetch_min_bytes=fetch_min_bytes, - max_partition_fetch_bytes=max_partition_fetch_bytes, - auto_offset_reset=auto_offset_reset, - auto_commit=auto_commit, - auto_commit_interval_ms=auto_commit_interval_ms, - check_crcs=check_crcs, - partition_assignment_strategy=partition_assignment_strategy, - max_poll_interval_ms=max_poll_interval_ms, - session_timeout_ms=session_timeout_ms, - heartbeat_interval_ms=heartbeat_interval_ms, - isolation_level=isolation_level, - max_records=max_records, - batch=batch, - # basic args - dependencies=dependencies, - parser=parser, - decoder=decoder, - middlewares=middlewares, - filter=filter, - no_reply=no_reply, - # AsyncAPI args - title=title, - description=description, - include_in_schema=include_in_schema, - # FastDepends args - retry=retry, - no_ack=no_ack, - ) - - -class KafkaRouter( - KafkaRegistrator, - BrokerRouter[ - Union[ - "Message", - Tuple["Message", ...], - ] - ], -): - """Includable to KafkaBroker router.""" - - def __init__( - self, - prefix: Annotated[ - str, - Doc("String prefix to add to all subscribers queues."), - ] = "", - handlers: Annotated[ - Iterable[KafkaRoute], - Doc("Route object to include."), - ] = (), - *, - dependencies: Annotated[ - Iterable["Depends"], - Doc( - "Dependencies list (`[Depends(),]`) to apply to all routers' publishers/subscribers." - ), - ] = (), - middlewares: Annotated[ - Sequence[ - Union[ - "BrokerMiddleware[Message]", - "BrokerMiddleware[Tuple[Message, ...]]", - ] - ], - Doc("Router middlewares to apply to all routers' publishers/subscribers."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **Message** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - include_in_schema: Annotated[ - Optional[bool], - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = None, - ) -> None: - super().__init__( - handlers=handlers, - # basic args - prefix=prefix, - dependencies=dependencies, - middlewares=middlewares, # type: ignore[arg-type] - parser=parser, - decoder=decoder, - include_in_schema=include_in_schema, - ) diff --git a/faststream/confluent/schemas/params.py b/faststream/confluent/schemas/params.py deleted file mode 100644 index cdde520f3d..0000000000 --- a/faststream/confluent/schemas/params.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import List, Literal, Union - -from typing_extensions import Required, TypedDict - -SecurityOptions = TypedDict( - "SecurityOptions", - { - "sasl.mechanism": Required[ - Literal[ - "PLAIN", - "GSSAPI", - "SCRAM-SHA-256", - "SCRAM-SHA-512", - "OAUTHBEARER", - ] - ], - "sasl.password": str, - "sasl.username": str, - }, - total=False, -) - - -class ConsumerConnectionParams(TypedDict, total=False): - """A class to represent the connection parameters for a consumer.""" - - bootstrap_servers: Union[str, List[str]] - client_id: str - retry_backoff_ms: int - metadata_max_age_ms: int - security_protocol: Literal[ - "SSL", - "PLAINTEXT", - ] - connections_max_idle_ms: int - allow_auto_create_topics: bool - security_config: SecurityOptions diff --git a/faststream/confluent/schemas/partition.py b/faststream/confluent/schemas/partition.py index ace5c78ca3..c17dbc42d6 100644 --- a/faststream/confluent/schemas/partition.py +++ b/faststream/confluent/schemas/partition.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from confluent_kafka import TopicPartition as ConfluentPartition @@ -27,8 +27,8 @@ def __init__( topic: str, partition: int = -1, offset: int = -1001, - metadata: Optional[str] = None, - leader_epoch: Optional[int] = None, + metadata: str | None = None, + leader_epoch: int | None = None, ) -> None: self.topic = topic self.partition = partition @@ -47,3 +47,12 @@ def to_confluent(self) -> ConfluentPartition: if self.leader_epoch is not None: kwargs["leader_epoch"] = self.leader_epoch return ConfluentPartition(**kwargs) + + def add_prefix(self, prefix: str) -> "TopicPartition": + return TopicPartition( + topic=f"{prefix}{self.topic}", + partition=self.partition, + offset=self.offset, + metadata=self.metadata, + leader_epoch=self.leader_epoch, + ) diff --git a/faststream/confluent/security.py b/faststream/confluent/security.py index 3eef59cb89..9522e41e2e 100644 --- a/faststream/confluent/security.py +++ b/faststream/confluent/security.py @@ -1,5 +1,4 @@ -import ssl -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from faststream.exceptions import SetupError from faststream.security import ( @@ -12,85 +11,76 @@ ) if TYPE_CHECKING: - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict -def parse_security(security: Optional[BaseSecurity]) -> "AnyDict": - if security and isinstance(security.ssl_context, ssl.SSLContext): - raise SetupError( - "ssl_context in not supported by confluent-kafka-python, please use config instead." - ) - +def parse_security(security: BaseSecurity | None) -> "AnyDict": if security is None: return {} - elif isinstance(security, SASLPlaintext): + + if security.ssl_context: + msg = "ssl_context is not supported by confluent-kafka-python, please use config instead." + raise SetupError(msg) + + if isinstance(security, SASLPlaintext): return _parse_sasl_plaintext(security) - elif isinstance(security, SASLScram256): + if isinstance(security, SASLScram256): return _parse_sasl_scram256(security) - elif isinstance(security, SASLScram512): + if isinstance(security, SASLScram512): return _parse_sasl_scram512(security) - elif isinstance(security, SASLOAuthBearer): + if isinstance(security, SASLOAuthBearer): return _parse_sasl_oauthbearer(security) - elif isinstance(security, SASLGSSAPI): + if isinstance(security, SASLGSSAPI): return _parse_sasl_gssapi(security) - elif isinstance(security, BaseSecurity): + if isinstance(security, BaseSecurity): return _parse_base_security(security) - else: - raise NotImplementedError(f"KafkaBroker does not support `{type(security)}`.") + + msg = f"KafkaBroker does not support `{type(security)}`." + raise NotImplementedError(msg) def _parse_base_security(security: BaseSecurity) -> "AnyDict": return { - "security_protocol": "SSL" if security.use_ssl else "PLAINTEXT", + "security.protocol": "ssl" if security.use_ssl else "plaintext", } def _parse_sasl_plaintext(security: SASLPlaintext) -> "AnyDict": return { - "security_protocol": "SASL_SSL" if security.use_ssl else "SASL_PLAINTEXT", - "security_config": { - "sasl.mechanism": "PLAIN", - "sasl.username": security.username, - "sasl.password": security.password, - }, + "security.protocol": "sasl_ssl" if security.use_ssl else "sasl_plaintext", + "sasl.mechanism": "PLAIN", + "sasl.username": security.username, + "sasl.password": security.password, } def _parse_sasl_scram256(security: SASLScram256) -> "AnyDict": return { - "security_protocol": "SASL_SSL" if security.use_ssl else "SASL_PLAINTEXT", - "security_config": { - "sasl.mechanism": "SCRAM-SHA-256", - "sasl.username": security.username, - "sasl.password": security.password, - }, + "security.protocol": "sasl_ssl" if security.use_ssl else "sasl_plaintext", + "sasl.mechanism": "SCRAM-SHA-256", + "sasl.username": security.username, + "sasl.password": security.password, } def _parse_sasl_scram512(security: SASLScram512) -> "AnyDict": return { - "security_protocol": "SASL_SSL" if security.use_ssl else "SASL_PLAINTEXT", - "security_config": { - "sasl.mechanism": "SCRAM-SHA-512", - "sasl.username": security.username, - "sasl.password": security.password, - }, + "security.protocol": "sasl_ssl" if security.use_ssl else "sasl_plaintext", + "sasl.mechanism": "SCRAM-SHA-512", + "sasl.username": security.username, + "sasl.password": security.password, } def _parse_sasl_oauthbearer(security: SASLOAuthBearer) -> "AnyDict": return { - "security_protocol": "SASL_SSL" if security.use_ssl else "SASL_PLAINTEXT", - "security_config": { - "sasl.mechanism": "OAUTHBEARER", - }, + "security.protocol": "sasl_ssl" if security.use_ssl else "sasl_plaintext", + "sasl.mechanism": "OAUTHBEARER", } def _parse_sasl_gssapi(security: SASLGSSAPI) -> "AnyDict": return { - "security_protocol": "SASL_SSL" if security.use_ssl else "SASL_PLAINTEXT", - "security_config": { - "sasl.mechanism": "GSSAPI", - }, + "security.protocol": "sasl_ssl" if security.use_ssl else "sasl_plaintext", + "sasl.mechanism": "GSSAPI", } diff --git a/faststream/confluent/subscriber/asyncapi.py b/faststream/confluent/subscriber/asyncapi.py deleted file mode 100644 index e5c1f3c623..0000000000 --- a/faststream/confluent/subscriber/asyncapi.py +++ /dev/null @@ -1,82 +0,0 @@ -from itertools import chain -from typing import ( - TYPE_CHECKING, - Dict, - Tuple, -) - -from faststream.asyncapi.schema import ( - Channel, - ChannelBinding, - CorrelationId, - Message, - Operation, -) -from faststream.asyncapi.schema.bindings import kafka -from faststream.asyncapi.utils import resolve_payloads -from faststream.broker.types import MsgType -from faststream.confluent.subscriber.usecase import ( - BatchSubscriber, - ConcurrentDefaultSubscriber, - DefaultSubscriber, - LogicSubscriber, -) - -if TYPE_CHECKING: - from confluent_kafka import Message as ConfluentMsg - - -class AsyncAPISubscriber(LogicSubscriber[MsgType]): - """A class to handle logic and async API operations.""" - - def get_name(self) -> str: - return f"{','.join(self.topics)}:{self.call_name}" - - def get_schema(self) -> Dict[str, Channel]: - channels = {} - - payloads = self.get_payloads() - - topics = chain(self.topics, {part.topic for part in self.partitions}) - - for t in topics: - handler_name = self.title_ or f"{t}:{self.call_name}" - - channels[handler_name] = Channel( - description=self.description, - subscribe=Operation( - message=Message( - title=f"{handler_name}:Message", - payload=resolve_payloads(payloads), - correlationId=CorrelationId( - location="$message.header#/correlation_id" - ), - ), - ), - bindings=ChannelBinding( - kafka=kafka.ChannelBinding(topic=t), - ), - ) - - return channels - - -class AsyncAPIDefaultSubscriber( - DefaultSubscriber, - AsyncAPISubscriber["ConfluentMsg"], -): - pass - - -class AsyncAPIBatchSubscriber( - BatchSubscriber, - AsyncAPISubscriber[Tuple["ConfluentMsg", ...]], -): - pass - - -class AsyncAPIConcurrentDefaultSubscriber( - ConcurrentDefaultSubscriber, - AsyncAPISubscriber["ConfluentMsg"], -): - pass diff --git a/faststream/confluent/subscriber/config.py b/faststream/confluent/subscriber/config.py new file mode 100644 index 0000000000..c87a663734 --- /dev/null +++ b/faststream/confluent/subscriber/config.py @@ -0,0 +1,63 @@ +from collections.abc import Iterable, Sequence +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from faststream._internal.configs import ( + SubscriberSpecificationConfig, + SubscriberUsecaseConfig, +) +from faststream._internal.constants import EMPTY +from faststream.confluent.configs import KafkaBrokerConfig +from faststream.middlewares import AckPolicy + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict + from faststream.confluent.schemas import TopicPartition + + +@dataclass(kw_only=True) +class KafkaSubscriberSpecificationConfig(SubscriberSpecificationConfig): + topics: Sequence[str] = field(default_factory=list) + partitions: Iterable["TopicPartition"] = field(default_factory=list) + + +@dataclass(kw_only=True) +class KafkaSubscriberConfig(SubscriberUsecaseConfig): + _outer_config: "KafkaBrokerConfig" = field(default_factory=KafkaBrokerConfig) + + topics: Sequence[str] = field(default_factory=list) + partitions: Sequence["TopicPartition"] = field(default_factory=list) + polling_interval: float = 0.1 + group_id: str | None = None + connection_data: "AnyDict" = field(default_factory=dict) + + _auto_commit: bool = field(default_factory=lambda: EMPTY, repr=False) + _no_ack: bool = field(default_factory=lambda: EMPTY, repr=False) + + def __post_init__(self) -> None: + if self.ack_first: + self.connection_data["enable_auto_commit"] = True + + @property + def ack_first(self) -> bool: + return self.__ack_policy is AckPolicy.ACK_FIRST + + @property + def ack_policy(self) -> AckPolicy: + if (policy := self.__ack_policy) is AckPolicy.ACK_FIRST: + return AckPolicy.DO_NOTHING + + return policy + + @property + def __ack_policy(self) -> AckPolicy: + if self._auto_commit is not EMPTY and self._auto_commit: + return AckPolicy.ACK_FIRST + + if self._no_ack is not EMPTY and self._no_ack: + return AckPolicy.DO_NOTHING + + if self._ack_policy is EMPTY: + return AckPolicy.ACK_FIRST + + return self._ack_policy diff --git a/faststream/confluent/subscriber/factory.py b/faststream/confluent/subscriber/factory.py index 748fdd0e39..7ec32bb54d 100644 --- a/faststream/confluent/subscriber/factory.py +++ b/faststream/confluent/subscriber/factory.py @@ -1,363 +1,148 @@ -from functools import wraps -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Dict, - Iterable, - Literal, - Optional, - Sequence, - Tuple, - Union, - cast, - overload, -) +import warnings +from collections.abc import Iterable, Sequence +from typing import TYPE_CHECKING -from faststream.confluent.publisher.asyncapi import ( - AsyncAPIBatchPublisher, - AsyncAPIDefaultPublisher, -) -from faststream.confluent.subscriber.asyncapi import ( - AsyncAPIBatchSubscriber, - AsyncAPIConcurrentDefaultSubscriber, - AsyncAPIDefaultSubscriber, -) +from faststream._internal.constants import EMPTY +from faststream._internal.endpoint.subscriber.call_item import CallsCollection from faststream.exceptions import SetupError +from faststream.middlewares import AckPolicy + +from .config import KafkaSubscriberConfig, KafkaSubscriberSpecificationConfig +from .specification import KafkaSubscriberSpecification +from .usecase import ( + BatchSubscriber, + ConcurrentDefaultSubscriber, + DefaultSubscriber, +) if TYPE_CHECKING: - from confluent_kafka import Message as ConfluentMsg - from fast_depends.dependencies import Depends - - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware + from faststream._internal.basic_types import AnyDict + from faststream.confluent.configs import KafkaBrokerConfig from faststream.confluent.schemas import TopicPartition - from faststream.types import AnyDict - - -@overload -def create_subscriber( - *topics: str, - partitions: Sequence["TopicPartition"], - polling_interval: float, - batch: Literal[True], - max_records: Optional[int], - # Kafka information - group_id: Optional[str], - connection_data: "AnyDict", - is_manual: bool, - # Subscriber args - no_ack: bool, - max_workers: int, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[Tuple[ConfluentMsg, ...]]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, -) -> "AsyncAPIBatchSubscriber": ... -@overload -def create_subscriber( - *topics: str, - partitions: Sequence["TopicPartition"], - polling_interval: float, - batch: Literal[False], - max_records: Optional[int], - # Kafka information - group_id: Optional[str], - connection_data: "AnyDict", - is_manual: bool, - # Subscriber args - no_ack: bool, - max_workers: int, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[ConfluentMsg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, -) -> Union[ - "AsyncAPIDefaultSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", -]: ... - - -@overload def create_subscriber( *topics: str, partitions: Sequence["TopicPartition"], polling_interval: float, batch: bool, - max_records: Optional[int], + max_records: int | None, # Kafka information - group_id: Optional[str], + group_id: str | None, connection_data: "AnyDict", - is_manual: bool, + auto_commit: bool, # Subscriber args + ack_policy: "AckPolicy", no_ack: bool, max_workers: int, no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Union[ - Sequence["BrokerMiddleware[Tuple[ConfluentMsg, ...]]"], - Sequence["BrokerMiddleware[ConfluentMsg]"], - ], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], + config: "KafkaBrokerConfig", + # Specification args + title_: str | None, + description_: str | None, include_in_schema: bool, -) -> Union[ - "AsyncAPIDefaultSubscriber", - "AsyncAPIBatchSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", -]: ... - - -def create_subscriber( - *topics: str, - partitions: Sequence["TopicPartition"], - polling_interval: float, - batch: bool, - max_records: Optional[int], - # Kafka information - group_id: Optional[str], - connection_data: "AnyDict", - is_manual: bool, - # Subscriber args - no_ack: bool, - max_workers: int, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Union[ - Sequence["BrokerMiddleware[Tuple[ConfluentMsg, ...]]"], - Sequence["BrokerMiddleware[ConfluentMsg]"], - ], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, -) -> Union[ - "AsyncAPIDefaultSubscriber", - "AsyncAPIBatchSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", -]: - if is_manual and max_workers > 1: - raise SetupError("Max workers not work with manual commit mode.") - - if batch: - return AsyncAPIBatchSubscriber( - *topics, +) -> BatchSubscriber | ConcurrentDefaultSubscriber | DefaultSubscriber: + _validate_input_for_misconfigure( + *topics, + group_id=group_id, + partitions=partitions, + ack_policy=ack_policy, + no_ack=no_ack, + auto_commit=auto_commit, + max_workers=max_workers, + ) + + subscriber_config = KafkaSubscriberConfig( + topics=topics, + partitions=partitions, + polling_interval=polling_interval, + group_id=group_id, + connection_data=connection_data, + no_reply=no_reply, + _outer_config=config, + _ack_policy=ack_policy, + # deprecated options to remove in 0.7.0 + _auto_commit=auto_commit, + _no_ack=no_ack, + ) + + calls = CallsCollection() + + specification = KafkaSubscriberSpecification( + _outer_config=config, + calls=calls, + specification_config=KafkaSubscriberSpecificationConfig( + topics=topics, partitions=partitions, - polling_interval=polling_interval, - max_records=max_records, - group_id=group_id, - connection_data=connection_data, - is_manual=is_manual, - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=cast( - "Sequence[BrokerMiddleware[Tuple[ConfluentMsg, ...]]]", - broker_middlewares, - ), title_=title_, description_=description_, include_in_schema=include_in_schema, + ), + ) + + if batch: + return BatchSubscriber( + subscriber_config, specification, calls, max_records=max_records ) - else: - if max_workers > 1: - return AsyncAPIConcurrentDefaultSubscriber( - *topics, - max_workers=max_workers, - partitions=partitions, - polling_interval=polling_interval, - group_id=group_id, - connection_data=connection_data, - is_manual=is_manual, - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=cast( - "Sequence[BrokerMiddleware[ConfluentMsg]]", - broker_middlewares, - ), - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - else: - return AsyncAPIDefaultSubscriber( - *topics, - partitions=partitions, - polling_interval=polling_interval, - group_id=group_id, - connection_data=connection_data, - is_manual=is_manual, - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=cast( - "Sequence[BrokerMiddleware[ConfluentMsg]]", - broker_middlewares, - ), - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + if max_workers > 1: + return ConcurrentDefaultSubscriber( + subscriber_config, specification, calls, max_workers=max_workers + ) -@overload -def create_publisher( - *, - batch: Literal[False], - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[ConfluentMsg]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - autoflush: bool = False, -) -> "AsyncAPIDefaultPublisher": ... + return DefaultSubscriber(subscriber_config, specification, calls) -@overload -def create_publisher( - *, - batch: Literal[True], - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[Tuple[ConfluentMsg, ...]]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - autoflush: bool = False, -) -> "AsyncAPIBatchPublisher": ... +def _validate_input_for_misconfigure( + *topics: str, + ack_policy: "AckPolicy", + auto_commit: bool, + no_ack: bool, + max_workers: int, + group_id: str | None, + partitions: Iterable["TopicPartition"], +) -> None: + if auto_commit is not EMPTY: + warnings.warn( + "`auto_commit` option was deprecated in prior to `ack_policy=AckPolicy.ACK_FIRST`. Scheduled to remove in 0.7.0", + category=DeprecationWarning, + stacklevel=4, + ) + if ack_policy is not EMPTY: + msg = "You can't use deprecated `auto_commit` and `ack_policy` simultaneously. Please, use `ack_policy` only." + raise SetupError(msg) -@overload -def create_publisher( - *, - batch: bool, - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Union[ - Sequence["BrokerMiddleware[Tuple[ConfluentMsg, ...]]"], - Sequence["BrokerMiddleware[ConfluentMsg]"], - ], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - autoflush: bool = False, -) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", -]: ... + ack_policy = AckPolicy.ACK_FIRST if auto_commit else AckPolicy.REJECT_ON_ERROR + if no_ack is not EMPTY: + warnings.warn( + "`no_ack` option was deprecated in prior to `ack_policy=AckPolicy.DO_NOTHING`. Scheduled to remove in 0.7.0", + category=DeprecationWarning, + stacklevel=4, + ) -def create_publisher( - *, - batch: bool, - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Union[ - Sequence["BrokerMiddleware[Tuple[ConfluentMsg, ...]]"], - Sequence["BrokerMiddleware[ConfluentMsg]"], - ], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - autoflush: bool = False, -) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", -]: - publisher: Union[AsyncAPIBatchPublisher, AsyncAPIDefaultPublisher] - if batch: - if key: - raise SetupError("You can't setup `key` with batch publisher") + if ack_policy is not EMPTY: + msg = "You can't use deprecated `no_ack` and `ack_policy` simultaneously. Please, use `ack_policy` only." + raise SetupError(msg) - publisher = AsyncAPIBatchPublisher( - topic=topic, - partition=partition, - headers=headers, - reply_to=reply_to, - broker_middlewares=cast( - "Sequence[BrokerMiddleware[Tuple[ConfluentMsg, ...]]]", - broker_middlewares, - ), - middlewares=middlewares, - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - else: - publisher = AsyncAPIDefaultPublisher( - key=key, - # basic args - topic=topic, - partition=partition, - headers=headers, - reply_to=reply_to, - broker_middlewares=cast( - "Sequence[BrokerMiddleware[ConfluentMsg]]", - broker_middlewares, - ), - middlewares=middlewares, - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + ack_policy = AckPolicy.DO_NOTHING if no_ack else EMPTY + + if ack_policy is EMPTY: + ack_policy = AckPolicy.ACK_FIRST - if autoflush: - default_publish: Callable[..., Awaitable[Optional[Any]]] = publisher.publish + if AckPolicy.ACK_FIRST is not AckPolicy.ACK_FIRST and max_workers > 1: + msg = "Max workers not work with manual commit mode." + raise SetupError(msg) - @wraps(default_publish) - async def autoflush_wrapper(*args: Any, **kwargs: Any) -> Optional[Any]: - result = await default_publish(*args, **kwargs) - await publisher.flush() - return result + if not topics and not partitions: + msg = "You should provide either `topics` or `partitions`." + raise SetupError(msg) - publisher.publish = autoflush_wrapper # type: ignore[method-assign] + if topics and partitions: + msg = "You can't provide both `topics` and `partitions`." + raise SetupError(msg) - return publisher + if not group_id and ack_policy is not AckPolicy.ACK_FIRST: + msg = "You must use `group_id` with manual commit mode." + raise SetupError(msg) diff --git a/faststream/confluent/subscriber/specification.py b/faststream/confluent/subscriber/specification.py new file mode 100644 index 0000000000..0d84ef7417 --- /dev/null +++ b/faststream/confluent/subscriber/specification.py @@ -0,0 +1,54 @@ + +from faststream._internal.endpoint.subscriber import SubscriberSpecification +from faststream.confluent.configs import KafkaBrokerConfig +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 .config import KafkaSubscriberSpecificationConfig + + +class KafkaSubscriberSpecification(SubscriberSpecification[KafkaBrokerConfig, KafkaSubscriberSpecificationConfig]): + @property + def topics(self) -> list[str]: + topics: set[str] = set() + + topics.update(f"{self._outer_config.prefix}{t}" for t in self.config.topics) + + topics.update(f"{self._outer_config.prefix}{p.topic}" for p in self.config.partitions) + + return list(topics) + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{','.join(self.topics)}:{self.call_name}" + + def get_schema(self) -> dict[str, SubscriberSpec]: + payloads = self.get_payloads() + + channels = {} + for t in self.topics: + handler_name = self.config.title_ or f"{t}:{self.call_name}" + + channels[handler_name] = SubscriberSpec( + description=self.description, + operation=Operation( + message=Message( + title=f"{handler_name}:Message", + payload=resolve_payloads(payloads), + ), + bindings=None, + ), + bindings=ChannelBinding( + kafka=kafka.ChannelBinding( + topic=t, + partitions=None, + replicas=None, + ), + ), + ) + + return channels diff --git a/faststream/confluent/subscriber/usecase.py b/faststream/confluent/subscriber/usecase.py index b435f35433..84540b2e2e 100644 --- a/faststream/confluent/subscriber/usecase.py +++ b/faststream/confluent/subscriber/usecase.py @@ -1,155 +1,91 @@ +import logging from abc import abstractmethod +from collections.abc import AsyncIterator, Sequence from typing import ( TYPE_CHECKING, Any, - Callable, - Dict, - Iterable, - List, Optional, - Sequence, - Tuple, ) import anyio from confluent_kafka import KafkaException, Message from typing_extensions import override -from faststream.broker.publisher.fake import FakePublisher -from faststream.broker.subscriber.mixins import ConcurrentMixin, TasksMixin -from faststream.broker.subscriber.usecase import SubscriberUsecase -from faststream.broker.types import MsgType -from faststream.broker.utils import process_msg +from faststream._internal.endpoint.subscriber import SubscriberUsecase +from faststream._internal.endpoint.subscriber.mixins import ConcurrentMixin, TasksMixin +from faststream._internal.endpoint.utils import process_msg +from faststream._internal.types import MsgType from faststream.confluent.parser import AsyncConfluentParser +from faststream.confluent.publisher.fake import KafkaFakePublisher from faststream.confluent.schemas import TopicPartition if TYPE_CHECKING: - from fast_depends.dependencies import Depends + from faststream._internal.endpoint.publisher import BasePublisherProto + from faststream._internal.endpoint.subscriber.call_item import CallsCollection + from faststream.confluent.configs import KafkaBrokerConfig + from faststream.confluent.helpers.client import AsyncConfluentConsumer + from faststream.message import StreamMessage - from faststream.broker.message import StreamMessage - from faststream.broker.publisher.proto import ProducerProto - from faststream.broker.types import ( - AsyncCallable, - BrokerMiddleware, - CustomCallable, - ) - from faststream.confluent.client import AsyncConfluentConsumer - from faststream.types import AnyDict, Decorator, LoggerProto + from .config import KafkaSubscriberConfig + from .specification import KafkaSubscriberSpecification class LogicSubscriber(TasksMixin, SubscriberUsecase[MsgType]): """A class to handle logic for consuming messages from Kafka.""" - topics: Sequence[str] - group_id: Optional[str] + _outer_config: "KafkaBrokerConfig" - builder: Optional[Callable[..., "AsyncConfluentConsumer"]] - consumer: Optional["AsyncConfluentConsumer"] + group_id: str | None - client_id: Optional[str] + consumer: Optional["AsyncConfluentConsumer"] + parser: AsyncConfluentParser def __init__( self, - *topics: str, - partitions: Sequence["TopicPartition"], - polling_interval: float, - # Kafka information - group_id: Optional[str], - connection_data: "AnyDict", - is_manual: bool, - # Subscriber args - default_parser: "AsyncCallable", - default_decoder: "AsyncCallable", - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[MsgType]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, + config: "KafkaSubscriberConfig", + specification: "KafkaSubscriberSpecification", + calls: "CallsCollection[MsgType]", ) -> None: - super().__init__( - default_parser=default_parser, - default_decoder=default_decoder, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + super().__init__(config, specification, calls) - self.__connection_data = connection_data + self.__connection_data = config.connection_data - self.group_id = group_id - self.topics = topics - self.partitions = partitions - self.is_manual = is_manual + self.group_id = config.group_id + + self._topics = config.topics + self._partitions = config.partitions self.consumer = None - self.polling_interval = polling_interval + self.polling_interval = config.polling_interval - # Setup it later - self.client_id = "" - self.builder = None + @property + def client_id(self) -> str | None: + return self._outer_config.client_id - @override - def setup( # type: ignore[override] - self, - *, - client_id: Optional[str], - builder: Callable[..., "AsyncConfluentConsumer"], - # basic args - logger: Optional["LoggerProto"], - producer: Optional["ProducerProto"], - graceful_timeout: Optional[float], - extra_context: "AnyDict", - # broker options - broker_parser: Optional["CustomCallable"], - broker_decoder: Optional["CustomCallable"], - # dependant args - apply_types: bool, - is_validate: bool, - _get_dependant: Optional[Callable[..., Any]], - _call_decorators: Iterable["Decorator"], - ) -> None: - self.client_id = client_id - self.builder = builder - - super().setup( - logger=logger, - producer=producer, - graceful_timeout=graceful_timeout, - extra_context=extra_context, - broker_parser=broker_parser, - broker_decoder=broker_decoder, - apply_types=apply_types, - is_validate=is_validate, - _get_dependant=_get_dependant, - _call_decorators=_call_decorators, - ) + @property + def topics(self) -> list[str]: + return [f"{self._outer_config.prefix}{t}" for t in self._topics] + + @property + def partitions(self) -> list[TopicPartition]: + return [p.add_prefix(self._outer_config.prefix) for p in self._partitions] @override async def start(self) -> None: """Start the consumer.""" - assert self.builder, "You should setup subscriber at first." # nosec B101 + await super().start() - self.consumer = consumer = self.builder( + self.consumer = consumer = self._outer_config.builder( *self.topics, partitions=self.partitions, group_id=self.group_id, client_id=self.client_id, **self.__connection_data, ) + self.parser._setup(consumer) await consumer.start() - await super().start() + self._post_start() if self.calls: self.add_task(self._consume()) @@ -166,7 +102,7 @@ async def get_one( self, *, timeout: float = 5.0, - ) -> "Optional[StreamMessage[MsgType]]": + ) -> "StreamMessage[MsgType] | None": assert self.consumer, "You should start subscriber at first." # nosec B101 assert ( # nosec B101 not self.calls @@ -174,26 +110,50 @@ async def get_one( raw_message = await self.consumer.getone(timeout=timeout) + context = self._outer_config.fd_config.context + return await process_msg( msg=raw_message, # type: ignore[arg-type] - middlewares=self._broker_middlewares, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), parser=self._parser, decoder=self._decoder, ) + @override + async def __aiter__(self) -> AsyncIterator["StreamMessage[MsgType]"]: # type: ignore[override] + assert self.consumer, "You should start subscriber at first." # nosec B101 + assert ( # nosec B101 + not self.calls + ), "You can't use iterator if subscriber has registered handlers." + + timeout = 5.0 + while True: + raw_message = await self.consumer.getone(timeout=timeout) + + if raw_message is None: + continue + + context = self._outer_config.fd_config.context + + yield await process_msg( + msg=raw_message, # type: ignore[arg-type] + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + def _make_response_publisher( self, message: "StreamMessage[Any]", - ) -> Sequence[FakePublisher]: - if self._producer is None: - return () - + ) -> Sequence["BasePublisherProto"]: return ( - FakePublisher( - self._producer.publish, - publish_kwargs={ - "topic": message.reply_to, - }, + KafkaFakePublisher( + self._outer_config.producer, + topic=message.reply_to, ), ) @@ -201,8 +161,8 @@ async def consume_one(self, msg: MsgType) -> None: await self.consume(msg) @abstractmethod - async def get_msg(self) -> Optional[MsgType]: - raise NotImplementedError() + async def get_msg(self) -> MsgType | None: + raise NotImplementedError async def _consume(self) -> None: assert self.consumer, "You should start subscriber at first." # nosec B101 @@ -211,9 +171,17 @@ async def _consume(self) -> None: while self.running: try: msg = await self.get_msg() - except KafkaException: # pragma: no cover # noqa: PERF203 + + except KafkaException as e: # pragma: no cover # noqa: PERF203 + self._log( + logging.ERROR, + message="Message fetch error", + exc_info=e, + ) + if connected: connected = False + await anyio.sleep(5) else: @@ -224,91 +192,34 @@ async def _consume(self) -> None: await self.consume_one(msg) @property - def topic_names(self) -> List[str]: - if self.topics: - return list(self.topics) - else: - return [f"{p.topic}-{p.partition}" for p in self.partitions] - - @staticmethod - def get_routing_hash(topics: Iterable[str], group_id: Optional[str] = None) -> int: - return hash("".join((*topics, group_id or ""))) - - def __hash__(self) -> int: - return self.get_routing_hash( - topics=self.topic_names, - group_id=self.group_id, - ) + def topic_names(self) -> list[str]: + topics = self.topics or (f"{p.topic}-{p.partition}" for p in self.partitions) + return [f"{self._outer_config.prefix}{t}" for t in topics] @staticmethod def build_log_context( message: Optional["StreamMessage[Any]"], topic: str, - group_id: Optional[str] = None, - ) -> Dict[str, str]: + group_id: str | None = None, + ) -> dict[str, str]: return { "topic": topic, "group_id": group_id or "", "message_id": getattr(message, "message_id", ""), } - def add_prefix(self, prefix: str) -> None: - self.topics = tuple("".join((prefix, t)) for t in self.topics) - - self.partitions = [ - TopicPartition( - topic="".join((prefix, p.topic)), - partition=p.partition, - offset=p.offset, - metadata=p.metadata, - leader_epoch=p.leader_epoch, - ) - for p in self.partitions - ] - class DefaultSubscriber(LogicSubscriber[Message]): def __init__( self, - *topics: str, - # Kafka information - partitions: Sequence["TopicPartition"], - polling_interval: float, - group_id: Optional[str], - connection_data: "AnyDict", - is_manual: bool, - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[Message]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, + config: "KafkaSubscriberConfig", + specification: "KafkaSubscriberSpecification", + calls: "CallsCollection[Message]", ) -> None: - super().__init__( - *topics, - partitions=partitions, - polling_interval=polling_interval, - group_id=group_id, - connection_data=connection_data, - is_manual=is_manual, - # subscriber args - default_parser=AsyncConfluentParser.parse_message, - default_decoder=AsyncConfluentParser.decode_message, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + self.parser = AsyncConfluentParser(is_manual=not config.ack_first) + config.decoder = self.parser.decode_message + config.parser = self.parser.parse_message + super().__init__(config, specification, calls) async def get_msg(self) -> Optional["Message"]: assert self.consumer, "You should setup subscriber at first." # nosec B101 @@ -317,7 +228,7 @@ async def get_msg(self) -> Optional["Message"]: def get_log_context( self, message: Optional["StreamMessage[Message]"], - ) -> Dict[str, str]: + ) -> dict[str, str]: if message is None: topic = ",".join(self.topic_names) else: @@ -330,70 +241,44 @@ def get_log_context( ) -class BatchSubscriber(LogicSubscriber[Tuple[Message, ...]]): +class ConcurrentDefaultSubscriber(ConcurrentMixin["Message"], DefaultSubscriber): + async def start(self) -> None: + await super().start() + self.start_consume_task() + + async def consume_one(self, msg: "Message") -> None: + await self._put_msg(msg) + + +class BatchSubscriber(LogicSubscriber[tuple[Message, ...]]): def __init__( self, - *topics: str, - partitions: Sequence["TopicPartition"], - polling_interval: float, - max_records: Optional[int], - # Kafka information - group_id: Optional[str], - connection_data: "AnyDict", - is_manual: bool, - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[Tuple[Message, ...]]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, + config: "KafkaSubscriberConfig", + specification: "KafkaSubscriberSpecification", + calls: "CallsCollection[tuple[Message, ...]]", + max_records: int | None, ) -> None: - self.max_records = max_records + self.parser = AsyncConfluentParser(is_manual=not config.ack_first) + config.decoder = self.parser.decode_message_batch + config.parser = self.parser.parse_message_batch + super().__init__(config, specification, calls) - super().__init__( - *topics, - partitions=partitions, - polling_interval=polling_interval, - group_id=group_id, - connection_data=connection_data, - is_manual=is_manual, - # subscriber args - default_parser=AsyncConfluentParser.parse_message_batch, - default_decoder=AsyncConfluentParser.decode_message_batch, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + self.max_records = max_records - async def get_msg(self) -> Optional[Tuple["Message", ...]]: + async def get_msg(self) -> tuple["Message", ...] | None: assert self.consumer, "You should setup subscriber at first." # nosec B101 - - messages = await self.consumer.getmany( - timeout=self.polling_interval, - max_records=self.max_records, + return ( + await self.consumer.getmany( + timeout=self.polling_interval, + max_records=self.max_records, + ) + or None ) - if not messages: # TODO: why we are sleeping here? - await anyio.sleep(self.polling_interval) - return None - - return messages - def get_log_context( self, - message: Optional["StreamMessage[Tuple[Message, ...]]"], - ) -> Dict[str, str]: + message: Optional["StreamMessage[tuple[Message, ...]]"], + ) -> dict[str, str]: if message is None: topic = ",".join(self.topic_names) else: @@ -404,54 +289,3 @@ def get_log_context( topic=topic, group_id=self.group_id, ) - - -class ConcurrentDefaultSubscriber(ConcurrentMixin[Message], DefaultSubscriber): - def __init__( - self, - *topics: str, - # Kafka information - partitions: Sequence["TopicPartition"], - polling_interval: float, - group_id: Optional[str], - connection_data: "AnyDict", - is_manual: bool, - # Subscriber args - max_workers: int, - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[Message]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - super().__init__( - *topics, - partitions=partitions, - polling_interval=polling_interval, - group_id=group_id, - connection_data=connection_data, - is_manual=is_manual, - # subscriber args - max_workers=max_workers, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - async def start(self) -> None: - await super().start() - self.start_consume_task() - - async def consume_one(self, msg: "Message") -> None: - await self._put_msg(msg) diff --git a/faststream/confluent/testing.py b/faststream/confluent/testing.py index 4e1e2f9488..e290811d83 100644 --- a/faststream/confluent/testing.py +++ b/faststream/confluent/testing.py @@ -1,26 +1,31 @@ -from datetime import datetime -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple +from collections.abc import Callable, Generator, Iterable, Iterator +from contextlib import ExitStack, contextmanager +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any, Optional from unittest.mock import AsyncMock, MagicMock import anyio from typing_extensions import override -from faststream.broker.message import encode_message, gen_cor_id -from faststream.broker.utils import resolve_custom_func +from faststream._internal.endpoint.utils import resolve_custom_func +from faststream._internal.testing.broker import TestBroker, change_producer from faststream.confluent.broker import KafkaBroker from faststream.confluent.parser import AsyncConfluentParser -from faststream.confluent.publisher.asyncapi import AsyncAPIBatchPublisher from faststream.confluent.publisher.producer import AsyncConfluentFastProducer +from faststream.confluent.publisher.usecase import BatchPublisher from faststream.confluent.schemas import TopicPartition -from faststream.confluent.subscriber.asyncapi import AsyncAPIBatchSubscriber +from faststream.confluent.subscriber.usecase import BatchSubscriber from faststream.exceptions import SubscriberNotFound -from faststream.testing.broker import TestBroker -from faststream.utils.functions import timeout_scope +from faststream.message import encode_message, gen_cor_id if TYPE_CHECKING: - from faststream.confluent.publisher.asyncapi import AsyncAPIPublisher + from fast_depends.library.serializer import SerializerProto + + from faststream._internal.basic_types import SendableMessage + from faststream.confluent.publisher.specification import SpecificationPublisher + from faststream.confluent.response import KafkaPublishCommand from faststream.confluent.subscriber.usecase import LogicSubscriber - from faststream.types import SendableMessage + __all__ = ("TestKafkaBroker",) @@ -28,24 +33,36 @@ class TestKafkaBroker(TestBroker[KafkaBroker]): """A class to test Kafka brokers.""" + @contextmanager + def _patch_producer(self, broker: KafkaBroker) -> Iterator[None]: + fake_producer = FakeProducer(broker) + + with ExitStack() as es: + es.enter_context( + change_producer(broker.config.broker_config, fake_producer) + ) + yield + @staticmethod async def _fake_connect( # type: ignore[override] broker: KafkaBroker, *args: Any, **kwargs: Any, ) -> Callable[..., AsyncMock]: - broker._producer = FakeProducer(broker) + broker.config.broker_config.admin.admin_client = MagicMock() return _fake_connection @staticmethod def create_publisher_fake_subscriber( broker: KafkaBroker, - publisher: "AsyncAPIPublisher[Any]", - ) -> Tuple["LogicSubscriber[Any]", bool]: - sub: Optional[LogicSubscriber[Any]] = None - for handler in broker._subscribers.values(): + publisher: "SpecificationPublisher[Any, Any]", + ) -> tuple["LogicSubscriber[Any]", bool]: + sub: LogicSubscriber[Any] | None = None + for handler in broker.subscribers: if _is_handler_matches( - handler, topic=publisher.topic, partition=publisher.partition + handler, + topic=publisher.topic, + partition=publisher.partition, ): sub = handler break @@ -55,17 +72,18 @@ def create_publisher_fake_subscriber( if publisher.partition: tp = TopicPartition( - topic=publisher.topic, partition=publisher.partition + topic=publisher.topic, + partition=publisher.partition, ) sub = broker.subscriber( partitions=[tp], - batch=isinstance(publisher, AsyncAPIBatchPublisher), + batch=isinstance(publisher, BatchPublisher), auto_offset_reset="earliest", ) else: sub = broker.subscriber( publisher.topic, - batch=isinstance(publisher, AsyncAPIBatchPublisher), + batch=isinstance(publisher, BatchPublisher), auto_offset_reset="earliest", ) @@ -84,131 +102,106 @@ class FakeProducer(AsyncConfluentFastProducer): def __init__(self, broker: KafkaBroker) -> None: self.broker = broker - default = AsyncConfluentParser - + default = AsyncConfluentParser() self._parser = resolve_custom_func(broker._parser, default.parse_message) self._decoder = resolve_custom_func(broker._decoder, default.decode_message) + def __bool__(self) -> bool: + return True + + async def ping(self, timeout: float) -> None: + return True + @override async def publish( # type: ignore[override] self, - message: "SendableMessage", - topic: str, - key: Optional[bytes] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, - *, - no_confirm: bool = False, - reply_to: str = "", - rpc: bool = False, - rpc_timeout: Optional[float] = None, - raise_timeout: bool = False, - ) -> Optional[Any]: + cmd: "KafkaPublishCommand", + ) -> None: """Publish a message to the Kafka broker.""" incoming = build_message( - message=message, - topic=topic, - key=key, - partition=partition, - timestamp_ms=timestamp_ms, - headers=headers, - correlation_id=correlation_id or gen_cor_id(), - reply_to=reply_to, + message=cmd.body, + topic=cmd.destination, + key=cmd.key, + partition=cmd.partition, + timestamp_ms=cmd.timestamp_ms, + headers=cmd.headers, + correlation_id=cmd.correlation_id, + reply_to=cmd.reply_to, + serializer=self.broker.config.fd_config._serializer ) - return_value = None - - for handler in self.broker._subscribers.values(): # pragma: no branch - if _is_handler_matches(handler, topic, partition): - msg_to_send = ( - [incoming] - if isinstance(handler, AsyncAPIBatchSubscriber) - else incoming - ) - - with timeout_scope(rpc_timeout, raise_timeout): - response_msg = await self._execute_handler( - msg_to_send, topic, handler - ) - if rpc: - return_value = return_value or await self._decoder( - await self._parser(response_msg) - ) + for handler in _find_handler( + self.broker.subscribers, + cmd.destination, + cmd.partition, + ): + msg_to_send = ( + [incoming] if isinstance(handler, BatchSubscriber) else incoming + ) - return return_value + await self._execute_handler(msg_to_send, cmd.destination, handler) async def publish_batch( self, - *msgs: "SendableMessage", - topic: str, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - reply_to: str = "", - correlation_id: Optional[str] = None, - no_confirm: bool = False, + cmd: "KafkaPublishCommand", ) -> None: """Publish a batch of messages to the Kafka broker.""" - for handler in self.broker._subscribers.values(): # pragma: no branch - if _is_handler_matches(handler, topic, partition): - messages = ( - build_message( - message=message, - topic=topic, - partition=partition, - timestamp_ms=timestamp_ms, - headers=headers, - correlation_id=correlation_id or gen_cor_id(), - reply_to=reply_to, - ) - for message in msgs + for handler in _find_handler( + self.broker.subscribers, + cmd.destination, + cmd.partition, + ): + messages = ( + build_message( + message=message, + topic=cmd.destination, + partition=cmd.partition, + timestamp_ms=cmd.timestamp_ms, + headers=cmd.headers, + correlation_id=cmd.correlation_id, + reply_to=cmd.reply_to, + serializer=self.broker.config.fd_config._serializer ) + for message in cmd.batch_bodies + ) - if isinstance(handler, AsyncAPIBatchSubscriber): - await self._execute_handler(list(messages), topic, handler) - - else: - for m in messages: - await self._execute_handler(m, topic, handler) + if isinstance(handler, BatchSubscriber): + await self._execute_handler(list(messages), cmd.destination, handler) - return None + else: + for m in messages: + await self._execute_handler(m, cmd.destination, handler) @override async def request( # type: ignore[override] self, - message: "SendableMessage", - topic: str, - key: Optional[bytes] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, - *, - timeout: Optional[float] = 0.5, + cmd: "KafkaPublishCommand", ) -> "MockConfluentMessage": incoming = build_message( - message=message, - topic=topic, - key=key, - partition=partition, - timestamp_ms=timestamp_ms, - headers=headers, - correlation_id=correlation_id or gen_cor_id(), + message=cmd.body, + topic=cmd.destination, + key=cmd.key, + partition=cmd.partition, + timestamp_ms=cmd.timestamp_ms, + headers=cmd.headers, + correlation_id=cmd.correlation_id, + serializer=self.broker.config.fd_config._serializer ) - for handler in self.broker._subscribers.values(): # pragma: no branch - if _is_handler_matches(handler, topic, partition): - msg_to_send = ( - [incoming] - if isinstance(handler, AsyncAPIBatchSubscriber) - else incoming + for handler in _find_handler( + self.broker.subscribers, + cmd.destination, + cmd.partition, + ): + msg_to_send = ( + [incoming] if isinstance(handler, BatchSubscriber) else incoming + ) + + with anyio.fail_after(cmd.timeout): + return await self._execute_handler( + msg_to_send, cmd.destination, handler ) - with anyio.fail_after(timeout): - return await self._execute_handler(msg_to_send, topic, handler) - raise SubscriberNotFound async def _execute_handler( @@ -224,6 +217,7 @@ async def _execute_handler( message=result.body, headers=result.headers, correlation_id=result.correlation_id or gen_cor_id(), + serializer=self.broker.config.fd_config._serializer ) @@ -233,13 +227,13 @@ def __init__( raw_msg: bytes, topic: str, key: bytes, - headers: List[Tuple[str, bytes]], + headers: list[tuple[str, bytes]], offset: int, partition: int, timestamp_type: int, timestamp_ms: int, - error: Optional[str] = None, - ): + error: str | None = None, + ) -> None: self._raw_msg = raw_msg self._topic = topic self._key = key @@ -252,10 +246,10 @@ def __init__( def len(self) -> int: return len(self._raw_msg) - def error(self) -> Optional[str]: + def error(self) -> str | None: return self._error - def headers(self) -> List[Tuple[str, bytes]]: + def headers(self) -> list[tuple[str, bytes]]: return self._headers def key(self) -> bytes: @@ -267,7 +261,7 @@ def offset(self) -> int: def partition(self) -> int: return self._partition - def timestamp(self) -> Tuple[int, int]: + def timestamp(self) -> tuple[int, int]: return self._timestamp def topic(self) -> str: @@ -281,19 +275,20 @@ def build_message( message: "SendableMessage", topic: str, *, - correlation_id: str, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - key: Optional[bytes] = None, - headers: Optional[Dict[str, str]] = None, + correlation_id: str | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + key: bytes | None = None, + headers: dict[str, str] | None = None, reply_to: str = "", + serializer: Optional["SerializerProto"] = None ) -> MockConfluentMessage: """Build a mock confluent_kafka.Message for a sendable message.""" - msg, content_type = encode_message(message) + msg, content_type = encode_message(message, serializer) k = key or b"" headers = { "content-type": content_type or "", - "correlation_id": correlation_id, + "correlation_id": correlation_id or gen_cor_id(), "reply_to": reply_to, **(headers or {}), } @@ -307,7 +302,7 @@ def build_message( offset=0, partition=partition or 0, timestamp_type=1, - timestamp_ms=timestamp_ms or int(datetime.now().timestamp() * 1000), + timestamp_ms=timestamp_ms or int(datetime.now(timezone.utc).timestamp() * 1000), ) @@ -318,15 +313,31 @@ def _fake_connection(*args: Any, **kwargs: Any) -> AsyncMock: return mock +def _find_handler( + subscribers: Iterable["LogicSubscriber[Any]"], + topic: str, + partition: int | None, +) -> Generator["LogicSubscriber[Any]", None, None]: + published_groups = set() + for handler in subscribers: # pragma: no branch + if _is_handler_matches(handler, topic, partition): + if handler.group_id: + if handler.group_id in published_groups: + continue + else: + published_groups.add(handler.group_id) + yield handler + + def _is_handler_matches( handler: "LogicSubscriber[Any]", topic: str, - partition: Optional[int], + partition: int | None, ) -> bool: return bool( any( p.topic == topic and (partition is None or p.partition == partition) for p in handler.partitions ) - or topic in handler.topics + or topic in handler.topics, ) diff --git a/faststream/constants.py b/faststream/constants.py deleted file mode 100644 index d3f7c3e25d..0000000000 --- a/faststream/constants.py +++ /dev/null @@ -1,10 +0,0 @@ -from enum import Enum - -ContentType = str - - -class ContentTypes(str, Enum): - """A class to represent content types.""" - - text = "text/plain" - json = "application/json" diff --git a/faststream/exceptions.py b/faststream/exceptions.py index 2a12fc8416..ffdf640891 100644 --- a/faststream/exceptions.py +++ b/faststream/exceptions.py @@ -1,4 +1,6 @@ -from typing import Any, Iterable +from collections.abc import Iterable +from pprint import pformat +from typing import Any class FastStreamException(Exception): # noqa: N818 @@ -45,7 +47,7 @@ class AckMessage(HandlerException): extra_options (Any): Additional parameters that will be passed to `message.ack(**extra_options)` method. """ - def __init__(self, **extra_options: Any): + def __init__(self, **extra_options: Any) -> None: self.extra_options = extra_options super().__init__() @@ -64,7 +66,7 @@ class NackMessage(HandlerException): kwargs (Any): Additional parameters that will be passed to `message.nack(**extra_options)` method. """ - def __init__(self, **kwargs: Any): + def __init__(self, **kwargs: Any) -> None: self.extra_options = kwargs super().__init__() @@ -83,7 +85,7 @@ class RejectMessage(HandlerException): kwargs (Any): Additional parameters that will be passed to `message.reject(**extra_options)` method. """ - def __init__(self, **kwargs: Any): + def __init__(self, **kwargs: Any) -> None: self.extra_options = kwargs super().__init__() @@ -95,14 +97,25 @@ class SetupError(FastStreamException, ValueError): """Exception to raise at wrong method usage.""" -class ValidationError(FastStreamException, ValueError): +class StartupValidationError(FastStreamException, ValueError): """Exception to raise at startup hook validation error.""" - def __init__(self, fields: Iterable[str] = ()) -> None: - self.fields = fields + def __init__( + self, + missed_fields: Iterable[str] = (), + invalid_fields: Iterable[str] = (), + ) -> None: + self.missed_fields = missed_fields + self.invalid_fields = invalid_fields + + def __str__(self) -> str: + return ( + f"\n Incorrect options `{' / '.join(f'--{i}' for i in (*self.missed_fields, *self.invalid_fields))}`" + "\n You registered extra options in your application `lifespan/on_startup` hook, but set them wrong in CLI." + ) -class OperationForbiddenError(FastStreamException, NotImplementedError): +class FeatureNotSupportedException(FastStreamException, NotImplementedError): # noqa: N818 """Raises at planned NotImplemented operation call.""" @@ -110,9 +123,29 @@ class SubscriberNotFound(FastStreamException): """Raises as a service message or in tests.""" +class IncorrectState(FastStreamException): + """Raises in FSM at wrong state calling.""" + + +class ContextError(FastStreamException, KeyError): + """Raises if context exception occurred.""" + + def __init__(self, context: Any, field: str) -> None: + self.context = context + self.field = field + + def __str__(self) -> str: + return "".join( + ( + f"\n Key `{self.field}` not found in the context\n ", + pformat(self.context), + ), + ) + + WRONG_PUBLISH_ARGS = SetupError( "You should use `reply_to` to send response to long-living queue " - "and `rpc` to get response in sync mode." + "and `rpc` to get response in sync mode.", ) @@ -134,31 +167,32 @@ class SubscriberNotFound(FastStreamException): pip install watchfiles """ +SCHEMA_NOT_SUPPORTED = "`{schema_filename}` not supported. Make sure that your schema is valid and schema version supported by FastStream" INSTALL_FASTSTREAM_RABBIT = """ To use RabbitMQ with FastStream, please install dependencies:\n -pip install 'faststream[rabbit]' +pip install "faststream[rabbit]" """ INSTALL_FASTSTREAM_KAFKA = """ To use Apache Kafka with FastStream, please install dependencies:\n -pip install 'faststream[kafka]' +pip install "faststream[kafka]" """ INSTALL_FASTSTREAM_CONFLUENT = """ To use Confluent Kafka with FastStream, please install dependencies:\n -pip install 'faststream[confluent]' +pip install "faststream[confluent]" """ INSTALL_FASTSTREAM_REDIS = """ To use Redis with FastStream, please install dependencies:\n -pip install 'faststream[redis]' +pip install "faststream[redis]" """ INSTALL_FASTSTREAM_NATS = """ To use NATS with FastStream, please install dependencies:\n -pip install 'faststream[nats]' +pip install "faststream[nats]" """ INSTALL_UVICORN = """ diff --git a/faststream/kafka/__init__.py b/faststream/kafka/__init__.py index 4388b88ac3..7c7207ca78 100644 --- a/faststream/kafka/__init__.py +++ b/faststream/kafka/__init__.py @@ -1,15 +1,17 @@ +from faststream._internal.testing.app import TestApp + try: from aiokafka import TopicPartition - from faststream.testing.app import TestApp - from .annotations import KafkaMessage - from .broker import KafkaBroker + from .broker import KafkaBroker, KafkaPublisher, KafkaRoute, KafkaRouter from .response import KafkaResponse - from .router import KafkaPublisher, KafkaRoute, KafkaRouter from .testing import TestKafkaBroker except ImportError as e: + if "'aiokafka'" not in e.msg: + raise + from faststream.exceptions import INSTALL_FASTSTREAM_KAFKA raise ImportError(INSTALL_FASTSTREAM_KAFKA) from e diff --git a/faststream/kafka/annotations.py b/faststream/kafka/annotations.py index fc735bd439..1f5c70d524 100644 --- a/faststream/kafka/annotations.py +++ b/faststream/kafka/annotations.py @@ -1,11 +1,13 @@ +from typing import Annotated + from aiokafka import AIOKafkaConsumer -from typing_extensions import Annotated -from faststream.annotations import ContextRepo, Logger, NoCast +from faststream._internal.context import Context +from faststream.annotations import ContextRepo, Logger from faststream.kafka.broker import KafkaBroker as KB from faststream.kafka.message import KafkaMessage as KM from faststream.kafka.publisher.producer import AioKafkaFastProducer -from faststream.utils.context import Context +from faststream.params import NoCast __all__ = ( "ContextRepo", diff --git a/faststream/kafka/broker/__init__.py b/faststream/kafka/broker/__init__.py index b77a930a65..25ed570afd 100644 --- a/faststream/kafka/broker/__init__.py +++ b/faststream/kafka/broker/__init__.py @@ -1,3 +1,4 @@ -from faststream.kafka.broker.broker import KafkaBroker +from .broker import KafkaBroker +from .router import KafkaPublisher, KafkaRoute, KafkaRouter -__all__ = ("KafkaBroker",) +__all__ = ("KafkaBroker", "KafkaPublisher", "KafkaRoute", "KafkaRouter") diff --git a/faststream/kafka/broker/broker.py b/faststream/kafka/broker/broker.py index b5a70e6c4c..946e58a0d7 100644 --- a/faststream/kafka/broker/broker.py +++ b/faststream/kafka/broker/broker.py @@ -1,524 +1,349 @@ import logging -import warnings -from contextlib import suppress +from collections.abc import Callable, Iterable, Sequence from functools import partial from typing import ( TYPE_CHECKING, Any, - Callable, - Dict, - Iterable, - List, Literal, Optional, - Sequence, - Tuple, - Type, TypeVar, Union, + overload, ) import aiokafka -import aiokafka.admin import anyio from aiokafka.partitioner import DefaultPartitioner from aiokafka.producer.producer import _missing -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import override from faststream.__about__ import SERVICE_NAME -from faststream.broker.message import gen_cor_id -from faststream.exceptions import NOT_CONNECTED_YET -from faststream.kafka.broker.logging import KafkaLoggingBroker -from faststream.kafka.broker.registrator import KafkaRegistrator -from faststream.kafka.publisher.producer import AioKafkaFastProducer -from faststream.kafka.schemas.params import ( - AdminClientConnectionParams, - ConsumerConnectionParams, -) +from faststream._internal.broker import BrokerUsecase +from faststream._internal.constants import EMPTY +from faststream._internal.di import FastDependsConfig +from faststream._internal.utils.data import filter_by_dict +from faststream.exceptions import IncorrectState +from faststream.kafka.configs import KafkaBrokerConfig +from faststream.kafka.publisher.producer import AioKafkaFastProducerImpl +from faststream.kafka.response import KafkaPublishCommand +from faststream.kafka.schemas.params import ConsumerConnectionParams from faststream.kafka.security import parse_security -from faststream.types import EMPTY -from faststream.utils.data import filter_by_dict +from faststream.message import gen_cor_id +from faststream.response.publish_type import PublishType +from faststream.specification.schema import BrokerSpec + +from .logging import make_kafka_logger_state +from .registrator import KafkaRegistrator Partition = TypeVar("Partition") if TYPE_CHECKING: - from asyncio import AbstractEventLoop + import asyncio from types import TracebackType from aiokafka import ConsumerRecord from aiokafka.abc import AbstractTokenProvider - from aiokafka.admin.client import AIOKafkaAdminClient - from fast_depends.dependencies import Depends - from typing_extensions import TypedDict, Unpack + from aiokafka.structs import RecordMetadata + from fast_depends.dependencies import Dependant + from fast_depends.library.serializer import SerializerProto + from typing_extensions import TypedDict - from faststream.asyncapi import schema as asyncapi - from faststream.broker.types import ( + from faststream._internal.basic_types import ( + LoggerProto, + SendableMessage, + ) + from faststream._internal.broker.abc_broker import Registrator + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, ) + from faststream.kafka.message import KafkaMessage from faststream.security import BaseSecurity - from faststream.types import ( - AnyDict, - AsyncFunc, - Decorator, - LoggerProto, - SendableMessage, - ) + from faststream.specification.schema.extra import Tag, TagDict class KafkaInitKwargs(TypedDict, total=False): - request_timeout_ms: Annotated[ - int, - Doc("Client request timeout in milliseconds."), - ] - retry_backoff_ms: Annotated[ - int, - Doc("Milliseconds to backoff when retrying on errors."), - ] - metadata_max_age_ms: Annotated[ - int, - Doc( - """ - The period of time in milliseconds after - which we force a refresh of metadata even if we haven't seen any - partition leadership changes to proactively discover any new - brokers or partitions. - """ - ), - ] - connections_max_idle_ms: Annotated[ - int, - Doc( - """ - Close idle connections after the number - of milliseconds specified by this config. Specifying `None` will - disable idle checks. - """ - ), - ] + """Kafka broker initialization keyword arguments. + + Attributes: + request_timeout_ms: Client request timeout in milliseconds. + retry_backoff_ms: Milliseconds to backoff when retrying on errors. + metadata_max_age_ms: The period of time in milliseconds after + which we force a refresh of metadata even if we haven't seen any + partition leadership changes to proactively discover any new + brokers or partitions. + connections_max_idle_ms: Close idle connections after the number + of milliseconds specified by this config. Specifying `None` will + disable idle checks. + sasl_kerberos_service_name: SASL Kerberos service name. + sasl_kerberos_domain_name: SASL Kerberos domain name. + sasl_oauth_token_provider: OAuthBearer token provider instance. + loop: Event loop instance. + client_id: A name for this client. This string is passed in + each request to servers and can be used to identify specific + server-side log entries that correspond to this client. Also + submitted to :class:`~.consumer.group_coordinator.GroupCoordinator` + for logging with respect to consumer group administration. + acks: One of ``0``, ``1``, ``all``. The number of acknowledgments + the producer requires the leader to have received before considering a + request complete. This controls the durability of records that are + sent. The following settings are common: + + * ``0``: Producer will not wait for any acknowledgment from the server + at all. The message will immediately be added to the socket + buffer and considered sent. No guarantee can be made that the + server has received the record in this case, and the retries + configuration will not take effect (as the client won't + generally know of any failures). The offset given back for each + record will always be set to -1. + * ``1``: The broker leader will write the record to its local log but + will respond without awaiting full acknowledgement from all + followers. In this case should the leader fail immediately + after acknowledging the record but before the followers have + replicated it then the record will be lost. + * ``all``: The broker leader will wait for the full set of in-sync + replicas to acknowledge the record. This guarantees that the + record will not be lost as long as at least one in-sync replica + remains alive. This is the strongest available guarantee. + + If unset, defaults to ``acks=1``. If `enable_idempotence` is + :data:`True` defaults to ``acks=all``. + key_serializer: Used to convert user-supplied keys to bytes. + value_serializer: Used to convert user-supplied message values to bytes. + compression_type: The compression type for all data generated by the producer. + Compression is of full batches of data, so the efficacy of batching + will also impact the compression ratio (more batching means better + compression). + max_batch_size: Maximum size of buffered data per partition. + After this amount `send` coroutine will block until batch is drained. + partitioner: Callable used to determine which partition + each message is assigned to. Called (after key serialization): + ``partitioner(key_bytes, all_partitions, available_partitions)``. + The default partitioner implementation hashes each non-None key + using the same murmur2 algorithm as the Java client so that + messages with the same key are assigned to the same partition. + When a key is :data:`None`, the message is delivered to a random partition + (filtered to partitions with available leaders only, if possible). + max_request_size: The maximum size of a request. This is also + effectively a cap on the maximum record size. Note that the server + has its own cap on record size which may be different from this. + This setting will limit the number of record batches the producer + will send in a single request to avoid sending huge requests. + linger_ms: The producer groups together any records that arrive + in between request transmissions into a single batched request. + Normally this occurs only under load when records arrive faster + than they can be sent out. However in some circumstances the client + may want to reduce the number of requests even under moderate load. + This setting accomplishes this by adding a small amount of + artificial delay; that is, if first request is processed faster, + than `linger_ms`, producer will wait ``linger_ms - process_time``. + enable_idempotence: When set to `True`, the producer will + ensure that exactly one copy of each message is written in the + stream. If `False`, producer retries due to broker failures, + etc., may write duplicates of the retried message in the stream. + Note that enabling idempotence acks to set to ``all``. If it is not + explicitly set by the user it will be chosen. + transactional_id: Transactional ID for the producer. + transaction_timeout_ms: Transaction timeout in milliseconds. + """ + request_timeout_ms: int + retry_backoff_ms: int + metadata_max_age_ms: int + connections_max_idle_ms: int sasl_kerberos_service_name: str - sasl_kerberos_domain_name: Optional[str] - sasl_oauth_token_provider: Annotated[ - Optional[AbstractTokenProvider], - Doc("OAuthBearer token provider instance."), - ] - loop: Optional[AbstractEventLoop] - client_id: Annotated[ - Optional[str], - Doc( - """ - A name for this client. This string is passed in - each request to servers and can be used to identify specific - server-side log entries that correspond to this client. Also - submitted to :class:`~.consumer.group_coordinator.GroupCoordinator` - for logging with respect to consumer group administration. - """ - ), - ] + sasl_kerberos_domain_name: str | None + sasl_oauth_token_provider: AbstractTokenProvider | None + loop: asyncio.AbstractEventLoop | None + client_id: str | None # publisher args - acks: Annotated[ - Union[Literal[0, 1, -1, "all"], object], - Doc( - """ - One of ``0``, ``1``, ``all``. The number of acknowledgments - the producer requires the leader to have received before considering a - request complete. This controls the durability of records that are - sent. The following settings are common: - - * ``0``: Producer will not wait for any acknowledgment from the server - at all. The message will immediately be added to the socket - buffer and considered sent. No guarantee can be made that the - server has received the record in this case, and the retries - configuration will not take effect (as the client won't - generally know of any failures). The offset given back for each - record will always be set to -1. - * ``1``: The broker leader will write the record to its local log but - will respond without awaiting full acknowledgement from all - followers. In this case should the leader fail immediately - after acknowledging the record but before the followers have - replicated it then the record will be lost. - * ``all``: The broker leader will wait for the full set of in-sync - replicas to acknowledge the record. This guarantees that the - record will not be lost as long as at least one in-sync replica - remains alive. This is the strongest available guarantee. - - If unset, defaults to ``acks=1``. If `enable_idempotence` is - :data:`True` defaults to ``acks=all``. - """ - ), - ] - key_serializer: Annotated[ - Optional[Callable[[Any], bytes]], - Doc("Used to convert user-supplied keys to bytes."), - ] - value_serializer: Annotated[ - Optional[Callable[[Any], bytes]], - Doc("used to convert user-supplied message values to bytes."), - ] - compression_type: Annotated[ - Optional[Literal["gzip", "snappy", "lz4", "zstd"]], - Doc( - """ - The compression type for all data generated bythe producer. - Compression is of full batches of data, so the efficacy of batching - will also impact the compression ratio (more batching means better - compression). - """ - ), - ] - max_batch_size: Annotated[ - int, - Doc( - """ - Maximum size of buffered data per partition. - After this amount `send` coroutine will block until batch is drained. - """ - ), - ] - partitioner: Annotated[ - Callable[ - [bytes, List[Partition], List[Partition]], - Partition, - ], - Doc( - """ - Callable used to determine which partition - each message is assigned to. Called (after key serialization): - ``partitioner(key_bytes, all_partitions, available_partitions)``. - The default partitioner implementation hashes each non-None key - using the same murmur2 algorithm as the Java client so that - messages with the same key are assigned to the same partition. - When a key is :data:`None`, the message is delivered to a random partition - (filtered to partitions with available leaders only, if possible). - """ - ), - ] - max_request_size: Annotated[ - int, - Doc( - """ - The maximum size of a request. This is also - effectively a cap on the maximum record size. Note that the server - has its own cap on record size which may be different from this. - This setting will limit the number of record batches the producer - will send in a single request to avoid sending huge requests. - """ - ), - ] - linger_ms: Annotated[ - int, - Doc( - """ - The producer groups together any records that arrive - in between request transmissions into a single batched request. - Normally this occurs only under load when records arrive faster - than they can be sent out. However in some circumstances the client - may want to reduce the number of requests even under moderate load. - This setting accomplishes this by adding a small amount of - artificial delay; that is, if first request is processed faster, - than `linger_ms`, producer will wait ``linger_ms - process_time``. - """ - ), + acks: Literal[0, 1, -1, "all"] | object + key_serializer: Callable[[Any], bytes] | None + value_serializer: Callable[[Any], bytes] | None + compression_type: Literal["gzip", "snappy", "lz4", "zstd"] | None + max_batch_size: int + partitioner: Callable[ + [bytes, list[Partition], list[Partition]], + Partition, ] - enable_idempotence: Annotated[ - bool, - Doc( - """ - When set to `True`, the producer will - ensure that exactly one copy of each message is written in the - stream. If `False`, producer retries due to broker failures, - etc., may write duplicates of the retried message in the stream. - Note that enabling idempotence acks to set to ``all``. If it is not - explicitly set by the user it will be chosen. - """ - ), - ] - transactional_id: Optional[str] + max_request_size: int + linger_ms: int + enable_idempotence: bool + transactional_id: str | None transaction_timeout_ms: int class KafkaBroker( KafkaRegistrator, - KafkaLoggingBroker, + BrokerUsecase[ + Union[aiokafka.ConsumerRecord, tuple[aiokafka.ConsumerRecord, ...]], + Callable[..., aiokafka.AIOKafkaConsumer], + ], ): - url: List[str] - _producer: Optional["AioKafkaFastProducer"] - _admin_client: Optional["AIOKafkaAdminClient"] + url: list[str] def __init__( self, - bootstrap_servers: Annotated[ - Union[str, Iterable[str]], - Doc( - """ - A `host[:port]` string (or list of `host[:port]` strings) that the consumer should contact to bootstrap - initial cluster metadata. - - This does not have to be the full node list. - It just needs to have at least one broker that will respond to a - Metadata API Request. Default port is 9092. - """ - ), - ] = "localhost", + bootstrap_servers: str | Iterable[str] = "localhost", *, # both - request_timeout_ms: Annotated[ - int, - Doc("Client request timeout in milliseconds."), - ] = 40 * 1000, - retry_backoff_ms: Annotated[ - int, - Doc("Milliseconds to backoff when retrying on errors."), - ] = 100, - metadata_max_age_ms: Annotated[ - int, - Doc( - """ - The period of time in milliseconds after - which we force a refresh of metadata even if we haven't seen any - partition leadership changes to proactively discover any new - brokers or partitions. - """ - ), - ] = 5 * 60 * 1000, - connections_max_idle_ms: Annotated[ - int, - Doc( - """ - Close idle connections after the number - of milliseconds specified by this config. Specifying `None` will - disable idle checks. - """ - ), - ] = 9 * 60 * 1000, + request_timeout_ms: int = 40 * 1000, + retry_backoff_ms: int = 100, + metadata_max_age_ms: int = 5 * 60 * 1000, + connections_max_idle_ms: int = 9 * 60 * 1000, sasl_kerberos_service_name: str = "kafka", - sasl_kerberos_domain_name: Optional[str] = None, - sasl_oauth_token_provider: Annotated[ - Optional["AbstractTokenProvider"], - Doc("OAuthBearer token provider instance."), - ] = None, - loop: Optional["AbstractEventLoop"] = None, - client_id: Annotated[ - Optional[str], - Doc( - """ - A name for this client. This string is passed in - each request to servers and can be used to identify specific - server-side log entries that correspond to this client. Also - submitted to :class:`~.consumer.group_coordinator.GroupCoordinator` - for logging with respect to consumer group administration. - """ - ), - ] = SERVICE_NAME, + sasl_kerberos_domain_name: str | None = None, + sasl_oauth_token_provider: Optional["AbstractTokenProvider"] = None, + loop: Optional["asyncio.AbstractEventLoop"] = None, + client_id: str | None = SERVICE_NAME, # publisher args - acks: Annotated[ - Union[Literal[0, 1, -1, "all"], object], - Doc( - """ - One of ``0``, ``1``, ``all``. The number of acknowledgments - the producer requires the leader to have received before considering a - request complete. This controls the durability of records that are - sent. The following settings are common: - - * ``0``: Producer will not wait for any acknowledgment from the server - at all. The message will immediately be added to the socket - buffer and considered sent. No guarantee can be made that the - server has received the record in this case, and the retries - configuration will not take effect (as the client won't - generally know of any failures). The offset given back for each - record will always be set to -1. - * ``1``: The broker leader will write the record to its local log but - will respond without awaiting full acknowledgement from all - followers. In this case should the leader fail immediately - after acknowledging the record but before the followers have - replicated it then the record will be lost. - * ``all``: The broker leader will wait for the full set of in-sync - replicas to acknowledge the record. This guarantees that the - record will not be lost as long as at least one in-sync replica - remains alive. This is the strongest available guarantee. - - If unset, defaults to ``acks=1``. If `enable_idempotence` is - :data:`True` defaults to ``acks=all``. - """ - ), - ] = _missing, - key_serializer: Annotated[ - Optional[Callable[[Any], bytes]], - Doc("Used to convert user-supplied keys to bytes."), - ] = None, - value_serializer: Annotated[ - Optional[Callable[[Any], bytes]], - Doc("used to convert user-supplied message values to bytes."), - ] = None, - compression_type: Annotated[ - Optional[Literal["gzip", "snappy", "lz4", "zstd"]], - Doc( - """ - The compression type for all data generated bythe producer. - Compression is of full batches of data, so the efficacy of batching - will also impact the compression ratio (more batching means better - compression). - """ - ), - ] = None, - max_batch_size: Annotated[ - int, - Doc( - """ - Maximum size of buffered data per partition. - After this amount `send` coroutine will block until batch is drained. - """ - ), - ] = 16 * 1024, - partitioner: Annotated[ - Callable[ - [bytes, List[Partition], List[Partition]], - Partition, - ], - Doc( - """ - Callable used to determine which partition - each message is assigned to. Called (after key serialization): - ``partitioner(key_bytes, all_partitions, available_partitions)``. - The default partitioner implementation hashes each non-None key - using the same murmur2 algorithm as the Java client so that - messages with the same key are assigned to the same partition. - When a key is :data:`None`, the message is delivered to a random partition - (filtered to partitions with available leaders only, if possible). - """ - ), + acks: Literal[0, 1, -1, "all"] | object = _missing, + key_serializer: Callable[[Any], bytes] | None = None, + value_serializer: Callable[[Any], bytes] | None = None, + compression_type: Literal["gzip", "snappy", "lz4", "zstd"] | None = None, + max_batch_size: int = 16 * 1024, + partitioner: Callable[ + [bytes, list[Partition], list[Partition]], + Partition, ] = DefaultPartitioner(), - max_request_size: Annotated[ - int, - Doc( - """ - The maximum size of a request. This is also - effectively a cap on the maximum record size. Note that the server - has its own cap on record size which may be different from this. - This setting will limit the number of record batches the producer - will send in a single request to avoid sending huge requests. - """ - ), - ] = 1024 * 1024, - linger_ms: Annotated[ - int, - Doc( - """ - The producer groups together any records that arrive - in between request transmissions into a single batched request. - Normally this occurs only under load when records arrive faster - than they can be sent out. However in some circumstances the client - may want to reduce the number of requests even under moderate load. - This setting accomplishes this by adding a small amount of - artificial delay; that is, if first request is processed faster, - than `linger_ms`, producer will wait ``linger_ms - process_time``. - """ - ), - ] = 0, - enable_idempotence: Annotated[ - bool, - Doc( - """ - When set to `True`, the producer will - ensure that exactly one copy of each message is written in the - stream. If `False`, producer retries due to broker failures, - etc., may write duplicates of the retried message in the stream. - Note that enabling idempotence acks to set to ``all``. If it is not - explicitly set by the user it will be chosen. - """ - ), - ] = False, - transactional_id: Optional[str] = None, + max_request_size: int = 1024 * 1024, + linger_ms: int = 0, + enable_idempotence: bool = False, + transactional_id: str | None = None, transaction_timeout_ms: int = 60 * 1000, # broker base args - graceful_timeout: Annotated[ - Optional[float], - Doc( - "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down." - ), - ] = 15.0, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Custom decoder object."), - ] = None, - parser: Annotated[ - Optional["CustomCallable"], - Doc("Custom parser object."), - ] = None, - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies to apply to all broker subscribers."), - ] = (), - middlewares: Annotated[ - Sequence[ - Union[ - "BrokerMiddleware[ConsumerRecord]", - "BrokerMiddleware[Tuple[ConsumerRecord, ...]]", - ] - ], - Doc("Middlewares to apply to all broker publishers/subscribers."), + graceful_timeout: float | None = 15.0, + decoder: Optional["CustomCallable"] = None, + parser: Optional["CustomCallable"] = None, + dependencies: Iterable["Dependant"] = (), + middlewares: Sequence[ + "BrokerMiddleware[ConsumerRecord | tuple[ConsumerRecord, ...]]" ] = (), + routers: Sequence["Registrator[ConsumerRecord]"] = (), # AsyncAPI args - security: Annotated[ - Optional["BaseSecurity"], - Doc( - "Security options to connect broker and generate AsyncAPI server security information." - ), - ] = None, - asyncapi_url: Annotated[ - Union[str, Iterable[str], None], - Doc("AsyncAPI hardcoded server addresses. Use `servers` if not specified."), - ] = None, - protocol: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol."), - ] = None, - protocol_version: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol version."), - ] = "auto", - description: Annotated[ - Optional[str], - Doc("AsyncAPI server description."), - ] = None, - tags: Annotated[ - Optional[Iterable[Union["asyncapi.Tag", "asyncapi.TagDict"]]], - Doc("AsyncAPI server tags."), - ] = None, + security: Optional["BaseSecurity"] = None, + specification_url: str | Iterable[str] | None = None, + protocol: str | None = None, + protocol_version: str | None = "auto", + description: str | None = None, + tags: Iterable[Union["Tag", "TagDict"]] = (), # logging args - logger: Annotated[ - Optional["LoggerProto"], - Doc("User specified logger to pass into Context and log service messages."), - ] = EMPTY, - log_level: Annotated[ - int, - Doc("Service messages log level."), - ] = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - Doc("Default logger log format."), - ] = EMPTY, + logger: Optional["LoggerProto"] = EMPTY, + log_level: int = logging.INFO, # FastDepends args - apply_types: Annotated[ - bool, - Doc("Whether to use FastDepends or not."), - ] = True, - validate: Annotated[ - bool, - Doc("Whether to cast types using Pydantic validation."), - ] = True, - _get_dependant: Annotated[ - Optional[Callable[..., Any]], - Doc("Custom library dependant generator callback."), - ] = None, - _call_decorators: Annotated[ - Iterable["Decorator"], - Doc("Any custom decorator to apply to wrapped functions."), - ] = (), + apply_types: bool = True, + serializer: Optional["SerializerProto"] = EMPTY, ) -> None: + """Kafka broker constructor. + + Args: + bootstrap_servers (Union[str, Iterable[str]]): + A `host[:port]` string (or list of `host[:port]` strings) that the consumer should contact to bootstrap + initial cluster metadata. This does not have to be the full node list. + It just needs to have at least one broker that will respond to a + Metadata API Request. Default port is 9092. + request_timeout_ms (int): + Client request timeout in milliseconds. + retry_backoff_ms (int): + Milliseconds to backoff when retrying on errors. + metadata_max_age_ms (int): + The period of time in milliseconds after which we force a refresh of metadata even if we haven't seen any + partition leadership changes to proactively discover any new brokers or partitions. + connections_max_idle_ms (int): + Close idle connections after the number of milliseconds specified by this config. Specifying `None` will + disable idle checks. + sasl_kerberos_service_name (str): + Kerberos service name. + sasl_kerberos_domain_name (Optional[str]): + Kerberos domain name. + sasl_oauth_token_provider (Optional[AbstractTokenProvider]): + OAuthBearer token provider instance. + loop (Optional[asyncio.AbstractEventLoop]): + Event loop to use. + client_id (Optional[str]): + A name for this client. This string is passed in each request to servers and can be used to identify specific + server-side log entries that correspond to this client. Also submitted to :class:`~.consumer.group_coordinator.GroupCoordinator` + for logging with respect to consumer group administration. + acks (Union[Literal[0, 1, -1, "all"], object]): + One of ``0``, ``1``, ``all``. The number of acknowledgments the producer requires the leader to have received before considering a + request complete. This controls the durability of records that are sent. The following settings are common: + * ``0``: Producer will not wait for any acknowledgment from the server at all. The message will immediately be added to the socket + buffer and considered sent. No guarantee can be made that the server has received the record in this case, and the retries + configuration will not take effect (as the client won't generally know of any failures). The offset given back for each + record will always be set to -1. + * ``1``: The broker leader will write the record to its local log but will respond without awaiting full acknowledgement from all + followers. In this case should the leader fail immediately after acknowledging the record but before the followers have + replicated it then the record will be lost. + * ``all``: The broker leader will wait for the full set of in-sync replicas to acknowledge the record. This guarantees that the + record will not be lost as long as at least one in-sync replica remains alive. This is the strongest available guarantee. + If unset, defaults to ``acks=1``. If `enable_idempotence` is :data:`True` defaults to ``acks=all``. + key_serializer (Optional[Callable[[Any], bytes]]): + Used to convert user-supplied keys to bytes. + value_serializer (Optional[Callable[[Any], bytes]]): + Used to convert user-supplied message values to bytes. + compression_type (Optional[Literal["gzip", "snappy", "lz4", "zstd"]]): + The compression type for all data generated by the producer. + Compression is of full batches of data, so the efficacy of batching will also impact the compression ratio (more batching means better compression). + max_batch_size (int): + Maximum size of buffered data per partition. After this amount `send` coroutine will block until batch is drained. + partitioner (Callable): + Callable used to determine which partition each message is assigned to. Called (after key serialization): + ``partitioner(key_bytes, all_partitions, available_partitions)``. + The default partitioner implementation hashes each non-None key using the same murmur2 algorithm as the Java client so that + messages with the same key are assigned to the same partition. When a key is :data:`None`, the message is delivered to a random partition + (filtered to partitions with available leaders only, if possible). + max_request_size (int): + The maximum size of a request. This is also effectively a cap on the maximum record size. Note that the server + has its own cap on record size which may be different from this. This setting will limit the number of record batches the producer + will send in a single request to avoid sending huge requests. + linger_ms (int): + The producer groups together any records that arrive in between request transmissions into a single batched request. + Normally this occurs only under load when records arrive faster than they can be sent out. However in some circumstances the client + may want to reduce the number of requests even under moderate load. This setting accomplishes this by adding a small amount of + artificial delay; that is, if first request is processed faster, than `linger_ms`, producer will wait ``linger_ms - process_time``. + enable_idempotence (bool): + When set to `True`, the producer will ensure that exactly one copy of each message is written in the stream. + If `False`, producer retries due to broker failures, etc., may write duplicates of the retried message in the stream. + Note that enabling idempotence acks to set to ``all``. If it is not explicitly set by the user it will be chosen. + transactional_id (Optional[str]): + Transactional id for the producer. + transaction_timeout_ms (int): + Transaction timeout in milliseconds. + graceful_timeout (Optional[float]): + Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down. + decoder (Optional[CustomCallable]): + Custom decoder object. + parser (Optional[CustomCallable]): + Custom parser object. + dependencies (Iterable[Dependant]): + Dependencies to apply to all broker subscribers. + middlewares (Sequence[BrokerMiddleware]): + Middlewares to apply to all broker publishers/subscribers. + routers (Sequence[Registrator]): + Routers to apply to broker. + security (Optional[BaseSecurity]): + Security options to connect broker and generate AsyncAPI server security information. + specification_url (Union[str, Iterable[str], None]): + AsyncAPI hardcoded server addresses. Use `servers` if not specified. + protocol (Optional[str]): + AsyncAPI server protocol. + protocol_version (Optional[str]): + AsyncAPI server protocol version. + description (Optional[str]): + AsyncAPI server description. + tags (Iterable[Union[Tag, TagDict]]): + AsyncAPI server tags. + logger (Optional[LoggerProto]): + User specified logger to pass into Context and log service messages. + log_level (int): + Service messages log level. + apply_types (bool): + Whether to use FastDepends or not. + serializer (Optional[SerializerProto]): + Serializer to use. + _get_dependant (Optional[Callable[..., Any]]): + Custom library dependant generator callback. + _call_decorators (Iterable[Decorator]): + Any custom decorator to apply to wrapped functions. + """ if protocol is None: if security is not None and security.use_ssl: protocol = "kafka-secure" @@ -531,15 +356,15 @@ def __init__( else list(bootstrap_servers) ) - if asyncapi_url is not None: - if isinstance(asyncapi_url, str): - asyncapi_url = [asyncapi_url] + if specification_url is not None: + if isinstance(specification_url, str): + specification_url = [specification_url] else: - asyncapi_url = list(asyncapi_url) + specification_url = list(specification_url) else: - asyncapi_url = servers + specification_url = servers - super().__init__( + connection_params = dict( bootstrap_servers=servers, # both args client_id=client_id, @@ -564,380 +389,322 @@ def __init__( enable_idempotence=enable_idempotence, transactional_id=transactional_id, transaction_timeout_ms=transaction_timeout_ms, - # Basic args - graceful_timeout=graceful_timeout, - dependencies=dependencies, - decoder=decoder, - parser=parser, - middlewares=middlewares, - # AsyncAPI args - description=description, - asyncapi_url=asyncapi_url, - protocol=protocol, - protocol_version=protocol_version, - security=security, - tags=tags, - # Logging args - logger=logger, - log_level=log_level, - log_fmt=log_fmt, - # FastDepends args - _get_dependant=_get_dependant, - _call_decorators=_call_decorators, - apply_types=apply_types, - validate=validate, + **parse_security(security), ) - self.client_id = client_id - self._producer = None - self._admin_client = None - - async def _close( - self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> None: - if self._producer is not None: # pragma: no branch - await self._producer.stop() - self._producer = None - if self._admin_client is not None: - await self._admin_client.close() - self._admin_client = None + consumer_options, _ = filter_by_dict( + ConsumerConnectionParams, connection_params + ) + builder = partial(aiokafka.AIOKafkaConsumer, **consumer_options) - await super()._close(exc_type, exc_val, exc_tb) + super().__init__( + **connection_params, + routers=routers, + config=KafkaBrokerConfig( + client_id=client_id, + builder=builder, + producer=AioKafkaFastProducerImpl( + parser=parser, + decoder=decoder, + ), + # both args, + broker_decoder=decoder, + broker_parser=parser, + broker_middlewares=middlewares, + logger=make_kafka_logger_state( + logger=logger, + log_level=log_level, + ), + fd_config=FastDependsConfig( + use_fastdepends=apply_types, + serializer=serializer, + ), + # subscriber args + graceful_timeout=graceful_timeout, + broker_dependencies=dependencies, + extra_context={ + "broker": self, + }, + ), + specification=BrokerSpec( + description=description, + url=specification_url, + protocol=protocol, + protocol_version=protocol_version, + security=security, + tags=tags, + ), + ) @override - async def connect( # type: ignore[override] - self, - bootstrap_servers: Annotated[ - Union[str, Iterable[str]], - Doc("Kafka addresses to connect."), - ] = EMPTY, - **kwargs: "Unpack[KafkaInitKwargs]", - ) -> Callable[..., aiokafka.AIOKafkaConsumer]: - """Connect to Kafka servers manually. - - Consumes the same with `KafkaBroker.__init__` arguments and overrides them. - To startup subscribers too you should use `broker.start()` after/instead this method. - """ - if bootstrap_servers is not EMPTY or kwargs: - warnings.warn( - "`KafkaBroker().connect(...) options were " - "deprecated in **FastStream 0.5.40**. " - "Please, use `KafkaBroker(...)` instead. " - "All these options will be removed in **FastStream 0.6.0**.", - DeprecationWarning, - stacklevel=2, - ) - - if bootstrap_servers is not EMPTY: - connect_kwargs: AnyDict = { - **kwargs, - "bootstrap_servers": bootstrap_servers, - } + async def _connect(self) -> Callable[..., aiokafka.AIOKafkaConsumer]: + await self.config.connect(**self._connection_kwargs) + return self.config.builder - else: - connect_kwargs = {**kwargs} - - return await super().connect(**connect_kwargs) - - @override - async def _connect( # type: ignore[override] + async def close( self, - *, - client_id: str, - **kwargs: Any, - ) -> Callable[..., aiokafka.AIOKafkaConsumer]: - security_params = parse_security(self.security) - kwargs.update(security_params) - - self._admin_client = aiokafka.admin.client.AIOKafkaAdminClient( - **filter_by_dict(AdminClientConnectionParams, kwargs), - ) - producer = aiokafka.AIOKafkaProducer( - **kwargs, - client_id=client_id, - ) - - await self._admin_client.start() - await producer.start() - self._producer = AioKafkaFastProducer( - producer=producer, - parser=self._parser, - decoder=self._decoder, - ) - - return partial( - aiokafka.AIOKafkaConsumer, - **filter_by_dict(ConsumerConnectionParams, kwargs), - ) + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> None: + await super().close(exc_type, exc_val, exc_tb) + await self.config.disconnect() + self._connection = None async def start(self) -> None: """Connect broker to Kafka and startup all subscribers.""" + await self.connect() await super().start() - for handler in self._subscribers.values(): - self._log( - f"`{handler.call_name}` waiting for messages", - extra=handler.get_log_context(None), - ) - await handler.start() - - @property - def _subscriber_setup_extra(self) -> "AnyDict": - return { - **super()._subscriber_setup_extra, - "client_id": self.client_id, - "builder": self._connection, - } + @overload + async def publish( + self, + message: "SendableMessage", + topic: str = "", + *, + key: bytes | Any | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + reply_to: str = "", + no_confirm: Literal[True], + ) -> "asyncio.Future[RecordMetadata]": ... + + @overload + async def publish( + self, + message: "SendableMessage", + topic: str = "", + *, + key: bytes | Any | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + reply_to: str = "", + no_confirm: Literal[False] = False, + ) -> "RecordMetadata": ... @override - async def publish( # type: ignore[override] + async def publish( self, - message: Annotated[ - "SendableMessage", - Doc("Message body to send."), - ], - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], + message: "SendableMessage", + topic: str = "", *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - timestamp_ms: Annotated[ - Optional[int], - Doc( - """ - Epoch milliseconds (from Jan 1 1970 UTC) to use as - the message timestamp. Defaults to current time. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc("Message headers to store metainformation."), - ] = None, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Reply message topic name to send response."), - ] = "", - no_confirm: Annotated[ - bool, - Doc("Do not wait for Kafka publish confirmation."), - ] = False, - # extra options to be compatible with test client - **kwargs: Any, - ) -> Optional[Any]: + key: bytes | Any | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + reply_to: str = "", + no_confirm: bool = False, + ) -> Union["asyncio.Future[RecordMetadata]", "RecordMetadata"]: """Publish message directly. This method allows you to publish message in not AsyncAPI-documented way. You can use it in another frameworks applications or to publish messages from time to time. Please, use `@broker.publisher(...)` or `broker.publisher(...).publish(...)` instead in a regular way. - """ - correlation_id = correlation_id or gen_cor_id() - return await super().publish( + Args: + message: + Message body to send. + topic: + Topic where the message will be published. + key: + A key to associate with the message. Can be used to + determine which partition to send the message to. If partition + is `None` (and producer's partitioner config is left as default), + then messages with the same key will be delivered to the same + partition (but if key is `None`, partition is chosen randomly). + Must be type `bytes`, or be serializable to bytes via configured + `key_serializer` + partition: + Specify a partition. If not set, the partition will be + selected using the configured `partitioner` + timestamp_ms: + Epoch milliseconds (from Jan 1 1970 UTC) to use as + the message timestamp. Defaults to current time. + headers: + Message headers to store metainformation. + correlation_id: + Manual message **correlation_id** setter. + **correlation_id** is a useful option to trace messages. + reply_to: + Reply message topic name to send response. + no_confirm: + Do not wait for Kafka publish confirmation. + + Returns: + `asyncio.Future[RecordMetadata]` if no_confirm = True. + `RecordMetadata` if no_confirm = False. + """ + cmd = KafkaPublishCommand( message, - producer=self._producer, topic=topic, key=key, partition=partition, timestamp_ms=timestamp_ms, headers=headers, - correlation_id=correlation_id, reply_to=reply_to, no_confirm=no_confirm, - **kwargs, + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.PUBLISH, ) + return await super()._basic_publish(cmd, producer=self.config.producer) @override async def request( # type: ignore[override] self, - message: Annotated[ - "SendableMessage", - Doc("Message body to send."), - ], - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], + message: "SendableMessage", + topic: str, *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to + key: bytes | Any | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + timeout: float = 0.5, + ) -> "KafkaMessage": + """Send a request message and wait for a response. + + Args: + message: Message body to send. + topic: Topic where the message will be published. + key: A key to associate with the message. Can be used to determine which partition to send the message to. If partition is `None` (and producer's partitioner config is left as default), then messages with the same key will be delivered to the same partition (but if key is `None`, partition is chosen randomly). Must be type `bytes`, or be serializable to bytes via configured `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be + partition: Specify a partition. If not set, the partition will be selected using the configured `partitioner`. - """ - ), - ] = None, - timestamp_ms: Annotated[ - Optional[int], - Doc( - """ - Epoch milliseconds (from Jan 1 1970 UTC) to use as + timestamp_ms: Epoch milliseconds (from Jan 1 1970 UTC) to use as the message timestamp. Defaults to current time. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc("Message headers to store metainformation."), - ] = None, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - timeout: Annotated[ - float, - Doc("Timeout to send RPC request."), - ] = 0.5, - ) -> Optional[Any]: - correlation_id = correlation_id or gen_cor_id() - - return await super().request( + headers: Message headers to store metainformation. + correlation_id: Manual message **correlation_id** setter. + **correlation_id** is a useful option to trace messages. + timeout: Timeout to send RPC request. + + Returns: + KafkaMessage: The response message. + """ + cmd = KafkaPublishCommand( message, - producer=self._producer, topic=topic, key=key, partition=partition, timestamp_ms=timestamp_ms, headers=headers, - correlation_id=correlation_id, timeout=timeout, + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.REQUEST, ) + msg: KafkaMessage = await super()._basic_request( + cmd, producer=self.config.producer + ) + return msg + + @overload async def publish_batch( self, - *msgs: Annotated[ - "SendableMessage", - Doc("Messages bodies to send."), - ], - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - timestamp_ms: Annotated[ - Optional[int], - Doc( - """ - Epoch milliseconds (from Jan 1 1970 UTC) to use as - the message timestamp. Defaults to current time. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc("Messages headers to store metainformation."), - ] = None, - reply_to: Annotated[ - str, - Doc("Reply message topic name to send response."), - ] = "", - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - no_confirm: Annotated[ - bool, - Doc("Do not wait for Kafka publish confirmation."), - ] = False, - ) -> None: - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - correlation_id = correlation_id or gen_cor_id() - - call: AsyncFunc = self._producer.publish_batch - - for m in self._middlewares[::-1]: - call = partial(m(None).publish_scope, call) + *messages: "SendableMessage", + topic: str = "", + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + correlation_id: str | None = None, + no_confirm: Literal[True], + ) -> "asyncio.Future[RecordMetadata]": ... + + @overload + async def publish_batch( + self, + *messages: "SendableMessage", + topic: str = "", + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + correlation_id: str | None = None, + no_confirm: Literal[False] = False, + ) -> "RecordMetadata": ... - await call( - *msgs, + async def publish_batch( + self, + *messages: "SendableMessage", + topic: str = "", + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + correlation_id: str | None = None, + no_confirm: bool = False, + ) -> Union["asyncio.Future[RecordMetadata]", "RecordMetadata"]: + """Publish a message batch as a single request to broker. + + Args: + *messages: + Messages bodies to send. + topic: + Topic where the message will be published. + partition: + Specify a partition. If not set, the partition will be + selected using the configured `partitioner` + timestamp_ms: + Epoch milliseconds (from Jan 1 1970 UTC) to use as + the message timestamp. Defaults to current time. + headers: + Message headers to store metainformation. + reply_to: + Reply message topic name to send response. + correlation_id: + Manual message **correlation_id** setter. + **correlation_id** is a useful option to trace messages. + no_confirm: + Do not wait for Kafka publish confirmation. + + Returns: + `asyncio.Future[RecordMetadata]` if no_confirm = True. + `RecordMetadata` if no_confirm = False. + """ + cmd = KafkaPublishCommand( + *messages, topic=topic, partition=partition, timestamp_ms=timestamp_ms, headers=headers, reply_to=reply_to, - correlation_id=correlation_id, no_confirm=no_confirm, + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.PUBLISH, ) + return await self._basic_publish_batch(cmd, producer=self.config.producer) + @override - async def ping(self, timeout: Optional[float]) -> bool: + async def ping(self, timeout: float | None) -> bool: sleep_time = (timeout or 10) / 10 - if self._admin_client is None: - return False - with anyio.move_on_after(timeout) as cancel_scope: while True: if cancel_scope.cancel_called: return False - with suppress(Exception): - await self._admin_client.describe_cluster() + try: + await self.config.admin_client.describe_cluster() + + except IncorrectState: + return False + + except Exception: + await anyio.sleep(sleep_time) + + else: return True - await anyio.sleep(sleep_time) return False diff --git a/faststream/kafka/broker/logging.py b/faststream/kafka/broker/logging.py index 6c0b940c9b..ba6d9a4e58 100644 --- a/faststream/kafka/broker/logging.py +++ b/faststream/kafka/broker/logging.py @@ -1,79 +1,76 @@ import logging -from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Tuple, Union +from functools import partial +from typing import TYPE_CHECKING -from typing_extensions import Annotated, deprecated - -from faststream.broker.core.usecase import BrokerUsecase -from faststream.log.logging import get_broker_logger -from faststream.types import EMPTY +from faststream._internal.logger import ( + DefaultLoggerStorage, + make_logger_state, +) +from faststream._internal.logger.logging import get_broker_logger if TYPE_CHECKING: - import aiokafka + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.context import ContextRepo + - from faststream.types import LoggerProto +class KafkaParamsStorage(DefaultLoggerStorage): + def __init__(self) -> None: + super().__init__() + self._max_topic_len = 4 + self._max_group_len = 0 -class KafkaLoggingBroker( - BrokerUsecase[ - Union["aiokafka.ConsumerRecord", Tuple["aiokafka.ConsumerRecord", ...]], - Callable[..., "aiokafka.AIOKafkaConsumer"], - ] -): - """A class that extends the LoggingMixin class and adds additional functionality for logging Kafka related information.""" + self.logger_log_level = logging.INFO - _max_topic_len: int - _max_group_len: int - __max_msg_id_ln: ClassVar[int] = 10 + def set_level(self, level: int) -> None: + self.logger_log_level = level - def __init__( - self, - *args: Any, - logger: Optional["LoggerProto"] = EMPTY, - log_level: int = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." + def register_subscriber(self, params: "AnyDict") -> None: + self._max_topic_len = max( + ( + self._max_topic_len, + len(params.get("topic", "")), ), - ] = EMPTY, - **kwargs: Any, - ) -> None: - """Initialize the class.""" - super().__init__( - *args, - logger=logger, - # TODO: generate unique logger names to not share between brokers - default_logger=get_broker_logger( + ) + self._max_group_len = max( + ( + self._max_group_len, + len(params.get("group_id", "")), + ), + ) + + def get_logger(self, *, context: "ContextRepo") -> "LoggerProto": + message_id_ln = 10 + + # TODO: generate unique logger names to not share between brokers + if not (lg := self._get_logger_ref()): + lg = get_broker_logger( name="kafka", default_context={ "topic": "", "group_id": "", }, - message_id_ln=self.__max_msg_id_ln, - ), - log_level=log_level, - log_fmt=log_fmt, - **kwargs, - ) - self._max_topic_len = 4 - self._max_group_len = 0 + message_id_ln=message_id_ln, + fmt="".join(( + "%(asctime)s %(levelname)-8s - ", + f"%(topic)-{self._max_topic_len}s | ", + ( + f"%(group_id)-{self._max_group_len}s | " + if self._max_group_len + else "" + ), + f"%(message_id)-{message_id_ln}s ", + "- %(message)s", + )), + context=context, + log_level=self.logger_log_level, + ) + self._logger_ref.add(lg) + + return lg - def get_fmt(self) -> str: - return ( - "%(asctime)s %(levelname)-8s - " - + f"%(topic)-{self._max_topic_len}s | " - + (f"%(group_id)-{self._max_group_len}s | " if self._max_group_len else "") - + f"%(message_id)-{self.__max_msg_id_ln}s " - + "- %(message)s" - ) - def _setup_log_context( - self, - *, - topic: str = "", - group_id: Optional[str] = None, - ) -> None: - """Set up log context.""" - self._max_topic_len = max((self._max_topic_len, len(topic))) - self._max_group_len = max((self._max_group_len, len(group_id or ""))) +make_kafka_logger_state = partial( + make_logger_state, + default_storage_cls=KafkaParamsStorage, +) diff --git a/faststream/kafka/broker/registrator.py b/faststream/kafka/broker/registrator.py index b06ba88c1e..515339fa33 100644 --- a/faststream/kafka/broker/registrator.py +++ b/faststream/kafka/broker/registrator.py @@ -1,14 +1,10 @@ +from collections.abc import Callable, Collection, Iterable, Sequence from typing import ( TYPE_CHECKING, + Annotated, Any, - Callable, - Collection, - Dict, - Iterable, Literal, Optional, - Sequence, - Tuple, Union, cast, overload, @@ -16,61 +12,63 @@ from aiokafka import ConsumerRecord from aiokafka.coordinator.assignors.roundrobin import RoundRobinPartitionAssignor -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import Doc, deprecated, override -from faststream.broker.core.abc import ABCBroker -from faststream.broker.utils import default_filter +from faststream._internal.broker.abc_broker import Registrator +from faststream._internal.constants import EMPTY from faststream.exceptions import SetupError -from faststream.kafka.subscriber.factory import create_publisher, create_subscriber +from faststream.kafka.publisher.factory import create_publisher +from faststream.kafka.subscriber.factory import create_subscriber +from faststream.middlewares import AckPolicy if TYPE_CHECKING: from aiokafka import TopicPartition from aiokafka.abc import ConsumerRebalanceListener from aiokafka.coordinator.assignors.abstract import AbstractPartitionAssignor - from fast_depends.dependencies import Depends + from fast_depends.dependencies import Dependant - from faststream.broker.types import ( + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, - Filter, PublisherMiddleware, SubscriberMiddleware, ) + from faststream.kafka.configs import KafkaBrokerConfig from faststream.kafka.message import KafkaMessage - from faststream.kafka.publisher.asyncapi import ( - AsyncAPIBatchPublisher, - AsyncAPIDefaultPublisher, + from faststream.kafka.publisher.specification import ( + SpecificationBatchPublisher, + SpecificationDefaultPublisher, ) - from faststream.kafka.subscriber.asyncapi import ( - AsyncAPIBatchSubscriber, - AsyncAPIConcurrentBetweenPartitionsSubscriber, - AsyncAPIConcurrentDefaultSubscriber, - AsyncAPIDefaultSubscriber, + from faststream.kafka.subscriber.specification import ( + SpecificationBatchSubscriber, + SpecificationConcurrentBetweenPartitionsSubscriber, + SpecificationConcurrentDefaultSubscriber, + SpecificationDefaultSubscriber, ) class KafkaRegistrator( - ABCBroker[ + Registrator[ Union[ ConsumerRecord, - Tuple[ConsumerRecord, ...], + tuple[ConsumerRecord, ...], ] - ] + ], ): """Includable to KafkaBroker router.""" - _subscribers: Dict[ - int, + config: "KafkaBrokerConfig" + + _subscribers: list[ Union[ - "AsyncAPIBatchSubscriber", - "AsyncAPIDefaultSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", - "AsyncAPIConcurrentBetweenPartitionsSubscriber", - ], + "SpecificationBatchSubscriber", + "SpecificationDefaultSubscriber", + "SpecificationConcurrentDefaultSubscriber", + "SpecificationConcurrentBetweenPartitionsSubscriber", + ] ] - _publishers: Dict[ - int, - Union["AsyncAPIBatchPublisher", "AsyncAPIDefaultPublisher"], + _publishers: list[ + Union["SpecificationBatchPublisher", "SpecificationDefaultPublisher"], ] @overload # type: ignore[override] @@ -85,28 +83,28 @@ def subscriber( Doc("Whether to consume messages in batches or not."), ] = False, group_id: Annotated[ - Optional[str], + str | None, Doc( """ Name of the consumer group to join for dynamic partition assignment (if enabled), and to use for fetching and committing offsets. If `None`, auto-partition assignment (via group coordinator) and offset commits are disabled. - """ + """, ), ] = None, key_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "key and returns a deserialized one." + "key and returns a deserialized one.", ), ] = None, value_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "value and returns a deserialized value." + "value and returns a deserialized value.", ), ] = None, fetch_max_bytes: Annotated[ @@ -121,7 +119,7 @@ def subscriber( performs fetches to multiple brokers in parallel so memory usage will depend on the number of brokers containing partitions for the topic. - """ + """, ), ] = 50 * 1024 * 1024, fetch_min_bytes: Annotated[ @@ -131,7 +129,7 @@ def subscriber( Minimum amount of data the server should return for a fetch request, otherwise wait up to `fetch_max_wait_ms` for more data to accumulate. - """ + """, ), ] = 1, fetch_max_wait_ms: Annotated[ @@ -142,7 +140,7 @@ def subscriber( the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by `fetch_min_bytes`. - """ + """, ), ] = 500, max_partition_fetch_bytes: Annotated[ @@ -157,7 +155,7 @@ def subscriber( send messages larger than the consumer can fetch. If that happens, the consumer can get stuck trying to fetch a large message on a certain partition. - """ + """, ), ] = 1 * 1024 * 1024, auto_offset_reset: Annotated[ @@ -169,7 +167,7 @@ def subscriber( * `earliest` will move to the oldest available message * `latest` will move to the most recent * `none` will raise an exception so you can handle this case - """ + """, ), ] = "latest", auto_commit: Annotated[ @@ -178,15 +176,21 @@ def subscriber( """ If `True` the consumer's offset will be periodically committed in the background. - """ + """, ), - ] = True, + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, auto_commit_interval_ms: Annotated[ int, Doc( """ Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" + offset commits, if `auto_commit` is `True`.""", ), ] = 5 * 1000, check_crcs: Annotated[ @@ -197,7 +201,7 @@ def subscriber( consumed. This ensures no on-the-wire or on-disk corruption to the messages occurred. This check adds some overhead, so it may be disabled in cases seeking extreme performance. - """ + """, ), ] = True, partition_assignment_strategy: Annotated[ @@ -213,7 +217,7 @@ def subscriber( one. The coordinator will choose the old assignment strategy until all members have been updated. Then it will choose the new strategy. - """ + """, ), ] = (RoundRobinPartitionAssignor,), max_poll_interval_ms: Annotated[ @@ -226,11 +230,11 @@ def subscriber( rebalance in order to reassign the partitions to another consumer group member. If API methods block waiting for messages, that time does not count against this timeout. - """ + """, ), ] = 5 * 60 * 1000, rebalance_timeout_ms: Annotated[ - Optional[int], + int | None, Doc( """ The maximum time server will wait for this @@ -240,7 +244,7 @@ def subscriber( decouple this setting to allow finer tuning by users that use `ConsumerRebalanceListener` to delay rebalacing. Defaults to ``session_timeout_ms`` - """ + """, ), ] = None, session_timeout_ms: Annotated[ @@ -255,7 +259,7 @@ def subscriber( group and trigger a rebalance. The allowed range is configured with the **broker** configuration properties `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ + """, ), ] = 10 * 1000, heartbeat_interval_ms: Annotated[ @@ -271,7 +275,7 @@ def subscriber( should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances. - """ + """, ), ] = 3 * 1000, consumer_timeout_ms: Annotated[ @@ -281,16 +285,16 @@ def subscriber( Maximum wait timeout for background fetching routine. Mostly defines how fast the system will see rebalance and request new data for new partitions. - """ + """, ), ] = 200, max_poll_records: Annotated[ - Optional[int], + int | None, Doc( """ The maximum number of records returned in a single call by batch consumer. Has no limit by default. - """ + """, ), ] = None, exclude_internal_topics: Annotated[ @@ -301,7 +305,7 @@ def subscriber( (such as offsets) should be exposed to the consumer. If set to True the only way to receive records from an internal topic is subscribing to it. - """ + """, ), ] = True, isolation_level: Annotated[ @@ -331,7 +335,7 @@ def subscriber( to the high watermark when there are in flight transactions. Further, when in `read_committed` the seek_to_end method will return the LSO. See method docs below. - """ + """, ), ] = "read_uncommitted", batch_timeout_ms: Annotated[ @@ -342,11 +346,11 @@ def subscriber( data is not available in the buffer. If 0, returns immediately with any records that are available currently in the buffer, else returns empty. - """ + """, ), ] = 200, max_records: Annotated[ - Optional[int], + int | None, Doc("Number of messages to consume as one batch."), ] = None, listener: Annotated[ @@ -374,15 +378,15 @@ def subscriber( to subscribe. It is guaranteed, however, that the partitions revoked/assigned through this interface are from topics subscribed in this call. - """ + """, ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ Pattern to match available topics. You must provide either topics or pattern, but not both. - """ + """, ), ] = None, partitions: Annotated[ @@ -391,13 +395,13 @@ def subscriber( """ An explicit partitions list to assign. You can't use 'topics' and 'partitions' in the same time. - """ + """, ), ] = (), # broker args dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), + Iterable["Dependant"], + Doc("Dependencies list (`[Dependant(),]`) to apply to the subscriber."), ] = (), parser: Annotated[ Optional["CustomCallable"], @@ -409,66 +413,44 @@ def subscriber( ] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - max_workers: Annotated[ - int, - Doc( - "Maximum number of messages being processed concurrently. With " - "`auto_commit=False` processing is concurrent between partitions and " - "sequential within a partition. With `auto_commit=False` maximum " - "concurrency is achieved when total number of workers across all " - "application instances running workers in the same consumer group " - "is equal to the number of partitions in the topic." - ), - ] = 1, - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = default_filter, - retry: Annotated[ + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), + no_ack: Annotated[ bool, - Doc("Whether to `nack` message at processing exception."), + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), deprecated( - "Deprecated in **FastStream 0.5.40**." - "Please, manage acknowledgement policy manually." - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" ), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), + str | None, + Doc("Specification subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Specification subscriber object description. " + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, - ) -> "AsyncAPIDefaultSubscriber": ... + ) -> "SpecificationDefaultSubscriber": ... @overload def subscriber( @@ -482,28 +464,28 @@ def subscriber( Doc("Whether to consume messages in batches or not."), ], group_id: Annotated[ - Optional[str], + str | None, Doc( """ Name of the consumer group to join for dynamic partition assignment (if enabled), and to use for fetching and committing offsets. If `None`, auto-partition assignment (via group coordinator) and offset commits are disabled. - """ + """, ), ] = None, key_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "key and returns a deserialized one." + "key and returns a deserialized one.", ), ] = None, value_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "value and returns a deserialized value." + "value and returns a deserialized value.", ), ] = None, fetch_max_bytes: Annotated[ @@ -518,7 +500,7 @@ def subscriber( performs fetches to multiple brokers in parallel so memory usage will depend on the number of brokers containing partitions for the topic. - """ + """, ), ] = 50 * 1024 * 1024, fetch_min_bytes: Annotated[ @@ -528,7 +510,7 @@ def subscriber( Minimum amount of data the server should return for a fetch request, otherwise wait up to `fetch_max_wait_ms` for more data to accumulate. - """ + """, ), ] = 1, fetch_max_wait_ms: Annotated[ @@ -539,7 +521,7 @@ def subscriber( the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by `fetch_min_bytes`. - """ + """, ), ] = 500, max_partition_fetch_bytes: Annotated[ @@ -554,7 +536,7 @@ def subscriber( send messages larger than the consumer can fetch. If that happens, the consumer can get stuck trying to fetch a large message on a certain partition. - """ + """, ), ] = 1 * 1024 * 1024, auto_offset_reset: Annotated[ @@ -566,7 +548,7 @@ def subscriber( * `earliest` will move to the oldest available message * `latest` will move to the most recent * `none` will raise an exception so you can handle this case - """ + """, ), ] = "latest", auto_commit: Annotated[ @@ -575,15 +557,21 @@ def subscriber( """ If `True` the consumer's offset will be periodically committed in the background. - """ + """, ), - ] = True, + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, auto_commit_interval_ms: Annotated[ int, Doc( """ Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" + offset commits, if `auto_commit` is `True`.""", ), ] = 5 * 1000, check_crcs: Annotated[ @@ -594,7 +582,7 @@ def subscriber( consumed. This ensures no on-the-wire or on-disk corruption to the messages occurred. This check adds some overhead, so it may be disabled in cases seeking extreme performance. - """ + """, ), ] = True, partition_assignment_strategy: Annotated[ @@ -610,7 +598,7 @@ def subscriber( one. The coordinator will choose the old assignment strategy until all members have been updated. Then it will choose the new strategy. - """ + """, ), ] = (RoundRobinPartitionAssignor,), max_poll_interval_ms: Annotated[ @@ -623,11 +611,11 @@ def subscriber( rebalance in order to reassign the partitions to another consumer group member. If API methods block waiting for messages, that time does not count against this timeout. - """ + """, ), ] = 5 * 60 * 1000, rebalance_timeout_ms: Annotated[ - Optional[int], + int | None, Doc( """ The maximum time server will wait for this @@ -637,7 +625,7 @@ def subscriber( decouple this setting to allow finer tuning by users that use `ConsumerRebalanceListener` to delay rebalacing. Defaults to ``session_timeout_ms`` - """ + """, ), ] = None, session_timeout_ms: Annotated[ @@ -652,7 +640,7 @@ def subscriber( group and trigger a rebalance. The allowed range is configured with the **broker** configuration properties `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ + """, ), ] = 10 * 1000, heartbeat_interval_ms: Annotated[ @@ -668,7 +656,7 @@ def subscriber( should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances. - """ + """, ), ] = 3 * 1000, consumer_timeout_ms: Annotated[ @@ -678,16 +666,16 @@ def subscriber( Maximum wait timeout for background fetching routine. Mostly defines how fast the system will see rebalance and request new data for new partitions. - """ + """, ), ] = 200, max_poll_records: Annotated[ - Optional[int], + int | None, Doc( """ The maximum number of records returned in a single call by batch consumer. Has no limit by default. - """ + """, ), ] = None, exclude_internal_topics: Annotated[ @@ -698,7 +686,7 @@ def subscriber( (such as offsets) should be exposed to the consumer. If set to True the only way to receive records from an internal topic is subscribing to it. - """ + """, ), ] = True, isolation_level: Annotated[ @@ -728,7 +716,7 @@ def subscriber( to the high watermark when there are in flight transactions. Further, when in `read_committed` the seek_to_end method will return the LSO. See method docs below. - """ + """, ), ] = "read_uncommitted", batch_timeout_ms: Annotated[ @@ -739,11 +727,11 @@ def subscriber( data is not available in the buffer. If 0, returns immediately with any records that are available currently in the buffer, else returns empty. - """ + """, ), ] = 200, max_records: Annotated[ - Optional[int], + int | None, Doc("Number of messages to consume as one batch."), ] = None, listener: Annotated[ @@ -771,15 +759,15 @@ def subscriber( to subscribe. It is guaranteed, however, that the partitions revoked/assigned through this interface are from topics subscribed in this call. - """ + """, ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ Pattern to match available topics. You must provide either topics or pattern, but not both. - """ + """, ), ] = None, partitions: Annotated[ @@ -788,13 +776,13 @@ def subscriber( """ An explicit partitions list to assign. You can't use 'topics' and 'partitions' in the same time. - """ + """, ), ] = (), # broker args dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), + Iterable["Dependant"], + Doc("Dependencies list (`[Dependant(),]`) to apply to the subscriber."), ] = (), parser: Annotated[ Optional["CustomCallable"], @@ -806,66 +794,44 @@ def subscriber( ] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - max_workers: Annotated[ - int, - Doc( - "Maximum number of messages being processed concurrently. With " - "`auto_commit=False` processing is concurrent between partitions and " - "sequential within a partition. With `auto_commit=False` maximum " - "concurrency is achieved when total number of workers across all " - "application instances running workers in the same consumer group " - "is equal to the number of partitions in the topic." - ), - ] = 1, - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = default_filter, - retry: Annotated[ + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), + no_ack: Annotated[ bool, - Doc("Whether to `nack` message at processing exception."), + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), deprecated( - "Deprecated in **FastStream 0.5.40**." - "Please, manage acknowledgement policy manually." - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" ), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), + str | None, + Doc("Specification subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Specification subscriber object description. " + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, - ) -> "AsyncAPIBatchSubscriber": ... + ) -> "SpecificationBatchSubscriber": ... @overload def subscriber( @@ -879,28 +845,28 @@ def subscriber( Doc("Whether to consume messages in batches or not."), ] = False, group_id: Annotated[ - Optional[str], + str | None, Doc( """ Name of the consumer group to join for dynamic partition assignment (if enabled), and to use for fetching and committing offsets. If `None`, auto-partition assignment (via group coordinator) and offset commits are disabled. - """ + """, ), ] = None, key_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "key and returns a deserialized one." + "key and returns a deserialized one.", ), ] = None, value_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "value and returns a deserialized value." + "value and returns a deserialized value.", ), ] = None, fetch_max_bytes: Annotated[ @@ -915,7 +881,7 @@ def subscriber( performs fetches to multiple brokers in parallel so memory usage will depend on the number of brokers containing partitions for the topic. - """ + """, ), ] = 50 * 1024 * 1024, fetch_min_bytes: Annotated[ @@ -925,7 +891,7 @@ def subscriber( Minimum amount of data the server should return for a fetch request, otherwise wait up to `fetch_max_wait_ms` for more data to accumulate. - """ + """, ), ] = 1, fetch_max_wait_ms: Annotated[ @@ -936,7 +902,7 @@ def subscriber( the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by `fetch_min_bytes`. - """ + """, ), ] = 500, max_partition_fetch_bytes: Annotated[ @@ -951,7 +917,7 @@ def subscriber( send messages larger than the consumer can fetch. If that happens, the consumer can get stuck trying to fetch a large message on a certain partition. - """ + """, ), ] = 1 * 1024 * 1024, auto_offset_reset: Annotated[ @@ -963,7 +929,7 @@ def subscriber( * `earliest` will move to the oldest available message * `latest` will move to the most recent * `none` will raise an exception so you can handle this case - """ + """, ), ] = "latest", auto_commit: Annotated[ @@ -972,15 +938,21 @@ def subscriber( """ If `True` the consumer's offset will be periodically committed in the background. - """ + """, ), - ] = True, + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, auto_commit_interval_ms: Annotated[ int, Doc( """ Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" + offset commits, if `auto_commit` is `True`.""", ), ] = 5 * 1000, check_crcs: Annotated[ @@ -991,7 +963,7 @@ def subscriber( consumed. This ensures no on-the-wire or on-disk corruption to the messages occurred. This check adds some overhead, so it may be disabled in cases seeking extreme performance. - """ + """, ), ] = True, partition_assignment_strategy: Annotated[ @@ -1007,7 +979,7 @@ def subscriber( one. The coordinator will choose the old assignment strategy until all members have been updated. Then it will choose the new strategy. - """ + """, ), ] = (RoundRobinPartitionAssignor,), max_poll_interval_ms: Annotated[ @@ -1020,11 +992,11 @@ def subscriber( rebalance in order to reassign the partitions to another consumer group member. If API methods block waiting for messages, that time does not count against this timeout. - """ + """, ), ] = 5 * 60 * 1000, rebalance_timeout_ms: Annotated[ - Optional[int], + int | None, Doc( """ The maximum time server will wait for this @@ -1034,7 +1006,7 @@ def subscriber( decouple this setting to allow finer tuning by users that use `ConsumerRebalanceListener` to delay rebalacing. Defaults to ``session_timeout_ms`` - """ + """, ), ] = None, session_timeout_ms: Annotated[ @@ -1049,7 +1021,7 @@ def subscriber( group and trigger a rebalance. The allowed range is configured with the **broker** configuration properties `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ + """, ), ] = 10 * 1000, heartbeat_interval_ms: Annotated[ @@ -1065,7 +1037,7 @@ def subscriber( should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances. - """ + """, ), ] = 3 * 1000, consumer_timeout_ms: Annotated[ @@ -1075,16 +1047,16 @@ def subscriber( Maximum wait timeout for background fetching routine. Mostly defines how fast the system will see rebalance and request new data for new partitions. - """ + """, ), ] = 200, max_poll_records: Annotated[ - Optional[int], + int | None, Doc( """ The maximum number of records returned in a single call by batch consumer. Has no limit by default. - """ + """, ), ] = None, exclude_internal_topics: Annotated[ @@ -1095,7 +1067,7 @@ def subscriber( (such as offsets) should be exposed to the consumer. If set to True the only way to receive records from an internal topic is subscribing to it. - """ + """, ), ] = True, isolation_level: Annotated[ @@ -1125,7 +1097,7 @@ def subscriber( to the high watermark when there are in flight transactions. Further, when in `read_committed` the seek_to_end method will return the LSO. See method docs below. - """ + """, ), ] = "read_uncommitted", batch_timeout_ms: Annotated[ @@ -1136,11 +1108,11 @@ def subscriber( data is not available in the buffer. If 0, returns immediately with any records that are available currently in the buffer, else returns empty. - """ + """, ), ] = 200, max_records: Annotated[ - Optional[int], + int | None, Doc("Number of messages to consume as one batch."), ] = None, listener: Annotated[ @@ -1168,15 +1140,15 @@ def subscriber( to subscribe. It is guaranteed, however, that the partitions revoked/assigned through this interface are from topics subscribed in this call. - """ + """, ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ Pattern to match available topics. You must provide either topics or pattern, but not both. - """ + """, ), ] = None, partitions: Annotated[ @@ -1185,13 +1157,13 @@ def subscriber( """ An explicit partitions list to assign. You can't use 'topics' and 'partitions' in the same time. - """ + """, ), ] = (), # broker args dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), + Iterable["Dependant"], + Doc("Dependencies list (`[Dependant(),]`) to apply to the subscriber."), ] = (), parser: Annotated[ Optional["CustomCallable"], @@ -1203,68 +1175,46 @@ def subscriber( ] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - max_workers: Annotated[ - int, - Doc( - "Maximum number of messages being processed concurrently. With " - "`auto_commit=False` processing is concurrent between partitions and " - "sequential within a partition. With `auto_commit=False` maximum " - "concurrency is achieved when total number of workers across all " - "application instances running workers in the same consumer group " - "is equal to the number of partitions in the topic." - ), - ] = 1, - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = default_filter, - retry: Annotated[ + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), + no_ack: Annotated[ bool, - Doc("Whether to `nack` message at processing exception."), + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), deprecated( - "Deprecated in **FastStream 0.5.40**." - "Please, manage acknowledgement policy manually." - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" ), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), + str | None, + Doc("Specification subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Specification subscriber object description. " + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, ) -> Union[ - "AsyncAPIDefaultSubscriber", - "AsyncAPIBatchSubscriber", + "SpecificationDefaultSubscriber", + "SpecificationBatchSubscriber", ]: ... @override @@ -1279,28 +1229,28 @@ def subscriber( Doc("Whether to consume messages in batches or not."), ] = False, group_id: Annotated[ - Optional[str], + str | None, Doc( """ Name of the consumer group to join for dynamic partition assignment (if enabled), and to use for fetching and committing offsets. If `None`, auto-partition assignment (via group coordinator) and offset commits are disabled. - """ + """, ), ] = None, key_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "key and returns a deserialized one." + "key and returns a deserialized one.", ), ] = None, value_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "value and returns a deserialized value." + "value and returns a deserialized value.", ), ] = None, fetch_max_bytes: Annotated[ @@ -1315,7 +1265,7 @@ def subscriber( performs fetches to multiple brokers in parallel so memory usage will depend on the number of brokers containing partitions for the topic. - """ + """, ), ] = 50 * 1024 * 1024, fetch_min_bytes: Annotated[ @@ -1325,7 +1275,7 @@ def subscriber( Minimum amount of data the server should return for a fetch request, otherwise wait up to `fetch_max_wait_ms` for more data to accumulate. - """ + """, ), ] = 1, fetch_max_wait_ms: Annotated[ @@ -1336,7 +1286,7 @@ def subscriber( the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by `fetch_min_bytes`. - """ + """, ), ] = 500, max_partition_fetch_bytes: Annotated[ @@ -1351,7 +1301,7 @@ def subscriber( send messages larger than the consumer can fetch. If that happens, the consumer can get stuck trying to fetch a large message on a certain partition. - """ + """, ), ] = 1 * 1024 * 1024, auto_offset_reset: Annotated[ @@ -1363,7 +1313,7 @@ def subscriber( * `earliest` will move to the oldest available message * `latest` will move to the most recent * `none` will raise an exception so you can handle this case - """ + """, ), ] = "latest", auto_commit: Annotated[ @@ -1372,15 +1322,21 @@ def subscriber( """ If `True` the consumer's offset will be periodically committed in the background. - """ + """, ), - ] = True, + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, auto_commit_interval_ms: Annotated[ int, Doc( """ Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" + offset commits, if `auto_commit` is `True`.""", ), ] = 5 * 1000, check_crcs: Annotated[ @@ -1391,7 +1347,7 @@ def subscriber( consumed. This ensures no on-the-wire or on-disk corruption to the messages occurred. This check adds some overhead, so it may be disabled in cases seeking extreme performance. - """ + """, ), ] = True, partition_assignment_strategy: Annotated[ @@ -1407,7 +1363,7 @@ def subscriber( one. The coordinator will choose the old assignment strategy until all members have been updated. Then it will choose the new strategy. - """ + """, ), ] = (RoundRobinPartitionAssignor,), max_poll_interval_ms: Annotated[ @@ -1420,11 +1376,11 @@ def subscriber( rebalance in order to reassign the partitions to another consumer group member. If API methods block waiting for messages, that time does not count against this timeout. - """ + """, ), ] = 5 * 60 * 1000, rebalance_timeout_ms: Annotated[ - Optional[int], + int | None, Doc( """ The maximum time server will wait for this @@ -1434,7 +1390,7 @@ def subscriber( decouple this setting to allow finer tuning by users that use `ConsumerRebalanceListener` to delay rebalacing. Defaults to ``session_timeout_ms`` - """ + """, ), ] = None, session_timeout_ms: Annotated[ @@ -1449,7 +1405,7 @@ def subscriber( group and trigger a rebalance. The allowed range is configured with the **broker** configuration properties `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ + """, ), ] = 10 * 1000, heartbeat_interval_ms: Annotated[ @@ -1465,7 +1421,7 @@ def subscriber( should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances. - """ + """, ), ] = 3 * 1000, consumer_timeout_ms: Annotated[ @@ -1475,16 +1431,16 @@ def subscriber( Maximum wait timeout for background fetching routine. Mostly defines how fast the system will see rebalance and request new data for new partitions. - """ + """, ), ] = 200, max_poll_records: Annotated[ - Optional[int], + int | None, Doc( """ The maximum number of records returned in a single call by batch consumer. Has no limit by default. - """ + """, ), ] = None, exclude_internal_topics: Annotated[ @@ -1495,7 +1451,7 @@ def subscriber( (such as offsets) should be exposed to the consumer. If set to True the only way to receive records from an internal topic is subscribing to it. - """ + """, ), ] = True, isolation_level: Annotated[ @@ -1525,7 +1481,7 @@ def subscriber( to the high watermark when there are in flight transactions. Further, when in `read_committed` the seek_to_end method will return the LSO. See method docs below. - """ + """, ), ] = "read_uncommitted", batch_timeout_ms: Annotated[ @@ -1536,11 +1492,11 @@ def subscriber( data is not available in the buffer. If 0, returns immediately with any records that are available currently in the buffer, else returns empty. - """ + """, ), ] = 200, max_records: Annotated[ - Optional[int], + int | None, Doc("Number of messages to consume as one batch."), ] = None, listener: Annotated[ @@ -1568,15 +1524,15 @@ def subscriber( to subscribe. It is guaranteed, however, that the partitions revoked/assigned through this interface are from topics subscribed in this call. - """ + """, ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ Pattern to match available topics. You must provide either topics or pattern, but not both. - """ + """, ), ] = None, partitions: Annotated[ @@ -1585,13 +1541,13 @@ def subscriber( """ An explicit partitions list to assign. You can't use 'topics' and 'partitions' in the same time. - """ + """, ), ] = (), # broker args dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), + Iterable["Dependant"], + Doc("Dependencies list (`[Dependant(),]`) to apply to the subscriber."), ] = (), parser: Annotated[ Optional["CustomCallable"], @@ -1603,6 +1559,10 @@ def subscriber( ] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[KafkaMessage]"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Subscriber middlewares to wrap incoming message processing."), ] = (), max_workers: Annotated[ @@ -1616,143 +1576,109 @@ def subscriber( "is equal to the number of partitions in the topic." ), ] = 1, - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), - deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ + no_ack: Annotated[ bool, - Doc("Whether to `nack` message at processing exception."), + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), deprecated( - "Deprecated in **FastStream 0.5.40**." - "Please, manage acknowledgement policy manually." - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" ), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), + str | None, + Doc("Specification subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Specification subscriber object description. " + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, ) -> Union[ - "AsyncAPIDefaultSubscriber", - "AsyncAPIBatchSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", - "AsyncAPIConcurrentBetweenPartitionsSubscriber", + "SpecificationDefaultSubscriber", + "SpecificationBatchSubscriber", + "SpecificationConcurrentDefaultSubscriber", + "SpecificationConcurrentBetweenPartitionsSubscriber", ]: - subscriber = super().subscriber( - create_subscriber( - *topics, - batch=batch, - max_workers=max_workers, - batch_timeout_ms=batch_timeout_ms, - max_records=max_records, - group_id=group_id, - listener=listener, - pattern=pattern, - connection_args={ - "key_deserializer": key_deserializer, - "value_deserializer": value_deserializer, - "fetch_max_wait_ms": fetch_max_wait_ms, - "fetch_max_bytes": fetch_max_bytes, - "fetch_min_bytes": fetch_min_bytes, - "max_partition_fetch_bytes": max_partition_fetch_bytes, - "auto_offset_reset": auto_offset_reset, - "enable_auto_commit": auto_commit, - "auto_commit_interval_ms": auto_commit_interval_ms, - "check_crcs": check_crcs, - "partition_assignment_strategy": partition_assignment_strategy, - "max_poll_interval_ms": max_poll_interval_ms, - "rebalance_timeout_ms": rebalance_timeout_ms, - "session_timeout_ms": session_timeout_ms, - "heartbeat_interval_ms": heartbeat_interval_ms, - "consumer_timeout_ms": consumer_timeout_ms, - "max_poll_records": max_poll_records, - "exclude_internal_topics": exclude_internal_topics, - "isolation_level": isolation_level, - }, - partitions=partitions, - is_manual=not auto_commit, - # subscriber args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=self._middlewares, - broker_dependencies=self._dependencies, - # AsyncAPI - title_=title, - description_=description, - include_in_schema=self._solve_include_in_schema(include_in_schema), - ) + sub = create_subscriber( + *topics, + batch=batch, + max_workers=max_workers, + batch_timeout_ms=batch_timeout_ms, + max_records=max_records, + group_id=group_id, + listener=listener, + pattern=pattern, + connection_args={ + "key_deserializer": key_deserializer, + "value_deserializer": value_deserializer, + "fetch_max_wait_ms": fetch_max_wait_ms, + "fetch_max_bytes": fetch_max_bytes, + "fetch_min_bytes": fetch_min_bytes, + "max_partition_fetch_bytes": max_partition_fetch_bytes, + "auto_offset_reset": auto_offset_reset, + "auto_commit_interval_ms": auto_commit_interval_ms, + "check_crcs": check_crcs, + "partition_assignment_strategy": partition_assignment_strategy, + "max_poll_interval_ms": max_poll_interval_ms, + "rebalance_timeout_ms": rebalance_timeout_ms, + "session_timeout_ms": session_timeout_ms, + "heartbeat_interval_ms": heartbeat_interval_ms, + "consumer_timeout_ms": consumer_timeout_ms, + "max_poll_records": max_poll_records, + "exclude_internal_topics": exclude_internal_topics, + "isolation_level": isolation_level, + }, + partitions=partitions, + # acknowledgement args + ack_policy=ack_policy, + no_ack=no_ack, + auto_commit=auto_commit, + # subscriber args + no_reply=no_reply, + config=self.config, + # Specification + title_=title, + description_=description, + include_in_schema=include_in_schema, ) + subscriber = super().subscriber(sub) + if batch: - return cast("AsyncAPIBatchSubscriber", subscriber).add_call( - filter_=filter, - parser_=parser or self._parser, - decoder_=decoder or self._decoder, - dependencies_=dependencies, - middlewares_=middlewares, - ) + subscriber = cast("SpecificationBatchSubscriber", subscriber) - else: - if max_workers > 1: - if not auto_commit: - return cast( - "AsyncAPIConcurrentBetweenPartitionsSubscriber", subscriber - ).add_call( - filter_=filter, - parser_=parser or self._parser, - decoder_=decoder or self._decoder, - dependencies_=dependencies, - middlewares_=middlewares, - ) - else: - return cast( - "AsyncAPIConcurrentDefaultSubscriber", subscriber - ).add_call( - filter_=filter, - parser_=parser or self._parser, - decoder_=decoder or self._decoder, - dependencies_=dependencies, - middlewares_=middlewares, - ) + elif max_workers > 1: + if auto_commit: + subscriber = cast( + "SpecificationConcurrentDefaultSubscriber", subscriber + ) else: - return cast("AsyncAPIDefaultSubscriber", subscriber).add_call( - filter_=filter, - parser_=parser or self._parser, - decoder_=decoder or self._decoder, - dependencies_=dependencies, - middlewares_=middlewares, + subscriber = cast( + "SpecificationConcurrentBetweenPartitionsSubscriber", subscriber ) + else: + subscriber = cast("SpecificationDefaultSubscriber", subscriber) + + return subscriber.add_call( + parser_=parser, + decoder_=decoder, + dependencies_=dependencies, + middlewares_=middlewares, + ) @overload # type: ignore[override] def publisher( @@ -1763,7 +1689,7 @@ def publisher( ], *, key: Annotated[ - Union[bytes, Any, None], + bytes | Any | None, Doc( """ A key to associate with the message. Can be used to @@ -1773,24 +1699,24 @@ def publisher( partition (but if key is `None`, partition is chosen randomly). Must be type `bytes`, or be serializable to bytes via configured `key_serializer`. - """ + """, ), ] = None, partition: Annotated[ - Optional[int], + int | None, Doc( """ Specify a partition. If not set, the partition will be selected using the configured `partitioner`. - """ + """, ), ] = None, headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc( "Message headers to store metainformation. " "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -1804,33 +1730,37 @@ def publisher( # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), + str | None, + Doc("Specification publisher object title."), ] = None, description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), + str | None, + Doc("Specification publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Specification publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, autoflush: Annotated[ bool, Doc("Whether to flush the producer or not on every publish call."), ] = False, - ) -> "AsyncAPIDefaultPublisher": ... + ) -> "SpecificationDefaultPublisher": ... @overload def publisher( @@ -1841,7 +1771,7 @@ def publisher( ], *, key: Annotated[ - Union[bytes, Any, None], + bytes | Any | None, Doc( """ A key to associate with the message. Can be used to @@ -1851,24 +1781,24 @@ def publisher( partition (but if key is `None`, partition is chosen randomly). Must be type `bytes`, or be serializable to bytes via configured `key_serializer`. - """ + """, ), ] = None, partition: Annotated[ - Optional[int], + int | None, Doc( """ Specify a partition. If not set, the partition will be selected using the configured `partitioner`. - """ + """, ), ] = None, headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc( "Message headers to store metainformation. " "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -1882,33 +1812,37 @@ def publisher( # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), + str | None, + Doc("Specification publisher object title."), ] = None, description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), + str | None, + Doc("Specification publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Specification publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, autoflush: Annotated[ bool, Doc("Whether to flush the producer or not on every publish call."), ] = False, - ) -> "AsyncAPIBatchPublisher": ... + ) -> "SpecificationBatchPublisher": ... @overload def publisher( @@ -1919,7 +1853,7 @@ def publisher( ], *, key: Annotated[ - Union[bytes, Any, None], + bytes | Any | None, Doc( """ A key to associate with the message. Can be used to @@ -1929,24 +1863,24 @@ def publisher( partition (but if key is `None`, partition is chosen randomly). Must be type `bytes`, or be serializable to bytes via configured `key_serializer`. - """ + """, ), ] = None, partition: Annotated[ - Optional[int], + int | None, Doc( """ Specify a partition. If not set, the partition will be selected using the configured `partitioner`. - """ + """, ), ] = None, headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc( "Message headers to store metainformation. " "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -1960,35 +1894,39 @@ def publisher( # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), + str | None, + Doc("Specification publisher object title."), ] = None, description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), + str | None, + Doc("Specification publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Specification publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, autoflush: Annotated[ bool, Doc("Whether to flush the producer or not on every publish call."), ] = False, ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", + "SpecificationBatchPublisher", + "SpecificationDefaultPublisher", ]: ... @override @@ -2000,7 +1938,7 @@ def publisher( ], *, key: Annotated[ - Union[bytes, Any, None], + bytes | Any | None, Doc( """ A key to associate with the message. Can be used to @@ -2010,24 +1948,24 @@ def publisher( partition (but if key is `None`, partition is chosen randomly). Must be type `bytes`, or be serializable to bytes via configured `key_serializer`. - """ + """, ), ] = None, partition: Annotated[ - Optional[int], + int | None, Doc( """ Specify a partition. If not set, the partition will be selected using the configured `partitioner`. - """ + """, ), ] = None, headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc( "Message headers to store metainformation. " "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -2041,37 +1979,41 @@ def publisher( # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), + str | None, + Doc("Specification publisher object title."), ] = None, description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), + str | None, + Doc("Specification publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Specification publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, autoflush: Annotated[ bool, Doc("Whether to flush the producer or not on every publish call."), ] = False, ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", + "SpecificationBatchPublisher", + "SpecificationDefaultPublisher", ]: - """Creates long-living and AsyncAPI-documented publisher object. + """Creates long-living and Specification-documented publisher object. You can use it as a handler decorator (handler should be decorated by `@broker.subscriber(...)` too) - `@broker.publisher(...)`. In such case publisher will publish your handler return value. @@ -2079,6 +2021,7 @@ def publisher( Or you can create a publisher object to call it lately - `broker.publisher(...).publish(...)`. """ publisher = create_publisher( + autoflush=autoflush, # batch flag batch=batch, # default args @@ -2089,20 +2032,20 @@ def publisher( headers=headers, reply_to=reply_to, # publisher-specific - broker_middlewares=self._middlewares, + config=self.config, middlewares=middlewares, - # AsyncAPI + # Specification title_=title, description_=description, schema_=schema, - include_in_schema=self._solve_include_in_schema(include_in_schema), - autoflush=autoflush, + include_in_schema=include_in_schema, ) + publisher = super().publisher(publisher) + if batch: - return cast("AsyncAPIBatchPublisher", super().publisher(publisher)) - else: - return cast("AsyncAPIDefaultPublisher", super().publisher(publisher)) + return cast("SpecificationBatchPublisher", publisher) + return cast("SpecificationDefaultPublisher", publisher) @override def include_router( @@ -2110,11 +2053,11 @@ def include_router( router: "KafkaRegistrator", # type: ignore[override] *, prefix: str = "", - dependencies: Iterable["Depends"] = (), + dependencies: Iterable["Dependant"] = (), middlewares: Iterable[ - "BrokerMiddleware[Union[ConsumerRecord, Tuple[ConsumerRecord, ...]]]" + "BrokerMiddleware[ConsumerRecord | tuple[ConsumerRecord, ...]]" ] = (), - include_in_schema: Optional[bool] = None, + include_in_schema: bool | None = None, ) -> None: if not isinstance(router, KafkaRegistrator): msg = ( diff --git a/faststream/kafka/broker/router.py b/faststream/kafka/broker/router.py new file mode 100644 index 0000000000..7be8903f20 --- /dev/null +++ b/faststream/kafka/broker/router.py @@ -0,0 +1,647 @@ +from collections.abc import Awaitable, Callable, Iterable, Sequence +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Literal, + Optional, + Union, +) + +from aiokafka.coordinator.assignors.roundrobin import RoundRobinPartitionAssignor +from typing_extensions import Doc, deprecated + +from faststream._internal.broker.router import ( + ArgsContainer, + BrokerRouter, + SubscriberRoute, +) +from faststream._internal.constants import EMPTY +from faststream.kafka.broker.registrator import KafkaRegistrator +from faststream.kafka.configs import KafkaBrokerConfig +from faststream.middlewares import AckPolicy + +if TYPE_CHECKING: + from aiokafka import ConsumerRecord, TopicPartition + from aiokafka.abc import ConsumerRebalanceListener + from aiokafka.coordinator.assignors.abstract import AbstractPartitionAssignor + from fast_depends.dependencies import Dependant + + from faststream._internal.basic_types import SendableMessage + from faststream._internal.broker.abc_broker import Registrator + from faststream._internal.types import ( + BrokerMiddleware, + CustomCallable, + PublisherMiddleware, + SubscriberMiddleware, + ) + from faststream.kafka.message import KafkaMessage + + +class KafkaPublisher(ArgsContainer): + """Delayed KafkaPublisher registration object. + + Just a copy of `KafkaRegistrator.publisher(...)` arguments. + """ + + def __init__( + self, + topic: Annotated[ + str, + Doc("Topic where the message will be published."), + ], + *, + key: Annotated[ + bytes | Any | None, + Doc( + """ + A key to associate with the message. Can be used to + determine which partition to send the message to. If partition + is `None` (and producer's partitioner config is left as default), + then messages with the same key will be delivered to the same + partition (but if key is `None`, partition is chosen randomly). + Must be type `bytes`, or be serializable to bytes via configured + `key_serializer`. + """, + ), + ] = None, + partition: Annotated[ + int | None, + Doc( + """ + Specify a partition. If not set, the partition will be + selected using the configured `partitioner`. + """, + ), + ] = None, + headers: Annotated[ + dict[str, str] | None, + Doc( + "Message headers to store metainformation. " + "**content-type** and **correlation_id** will be set automatically by framework anyway. " + "Can be overridden by `publish.headers` if specified.", + ), + ] = None, + reply_to: Annotated[ + str, + Doc("Topic name to send response."), + ] = "", + batch: Annotated[ + bool, + Doc("Whether to send messages in batches or not."), + ] = False, + # basic args + middlewares: Annotated[ + Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + Doc("Publisher middlewares to wrap outgoing messages."), + ] = (), + # AsyncAPI args + title: Annotated[ + str | None, + Doc("AsyncAPI publisher object title."), + ] = None, + description: Annotated[ + str | None, + Doc("AsyncAPI publisher object description."), + ] = None, + schema: Annotated[ + Any | None, + Doc( + "AsyncAPI publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = True, + ) -> None: + super().__init__( + topic=topic, + key=key, + partition=partition, + batch=batch, + headers=headers, + reply_to=reply_to, + # basic args + middlewares=middlewares, + # AsyncAPI args + title=title, + description=description, + schema=schema, + include_in_schema=include_in_schema, + ) + + +class KafkaRoute(SubscriberRoute): + """Class to store delayed KafkaBroker subscriber registration.""" + + def __init__( + self, + call: Annotated[ + Callable[..., "SendableMessage"] | Callable[..., Awaitable["SendableMessage"]], + Doc( + "Message handler function " + "to wrap the same with `@broker.subscriber(...)` way.", + ), + ], + *topics: Annotated[ + str, + Doc("Kafka topics to consume messages from."), + ], + publishers: Annotated[ + Iterable[KafkaPublisher], + Doc("Kafka publishers to broadcast the handler result."), + ] = (), + batch: Annotated[ + bool, + Doc("Whether to consume messages in batches or not."), + ] = False, + group_id: Annotated[ + str | None, + Doc( + """ + Name of the consumer group to join for dynamic + partition assignment (if enabled), and to use for fetching and + committing offsets. If `None`, auto-partition assignment (via + group coordinator) and offset commits are disabled. + """, + ), + ] = None, + key_deserializer: Annotated[ + Callable[[bytes], Any] | None, + Doc( + "Any callable that takes a raw message `bytes` " + "key and returns a deserialized one.", + ), + ] = None, + value_deserializer: Annotated[ + Callable[[bytes], Any] | None, + Doc( + "Any callable that takes a raw message `bytes` " + "value and returns a deserialized value.", + ), + ] = None, + fetch_max_bytes: Annotated[ + int, + Doc( + """ + The maximum amount of data the server should + return for a fetch request. This is not an absolute maximum, if + the first message in the first non-empty partition of the fetch + is larger than this value, the message will still be returned + to ensure that the consumer can make progress. NOTE: consumer + performs fetches to multiple brokers in parallel so memory + usage will depend on the number of brokers containing + partitions for the topic. + """, + ), + ] = 50 * 1024 * 1024, + fetch_min_bytes: Annotated[ + int, + Doc( + """ + Minimum amount of data the server should + return for a fetch request, otherwise wait up to + `fetch_max_wait_ms` for more data to accumulate. + """, + ), + ] = 1, + fetch_max_wait_ms: Annotated[ + int, + Doc( + """ + The maximum amount of time in milliseconds + the server will block before answering the fetch request if + there isn't sufficient data to immediately satisfy the + requirement given by `fetch_min_bytes`. + """, + ), + ] = 500, + max_partition_fetch_bytes: Annotated[ + int, + Doc( + """ + The maximum amount of data + per-partition the server will return. The maximum total memory + used for a request ``= #partitions * max_partition_fetch_bytes``. + This size must be at least as large as the maximum message size + the server allows or else it is possible for the producer to + send messages larger than the consumer can fetch. If that + happens, the consumer can get stuck trying to fetch a large + message on a certain partition. + """, + ), + ] = 1 * 1024 * 1024, + auto_offset_reset: Annotated[ + Literal["latest", "earliest", "none"], + Doc( + """ + A policy for resetting offsets on `OffsetOutOfRangeError` errors: + + * `earliest` will move to the oldest available message + * `latest` will move to the most recent + * `none` will raise an exception so you can handle this case + """, + ), + ] = "latest", + auto_commit: Annotated[ + bool, + Doc( + """ + If `True` the consumer's offset will be + periodically committed in the background. + """, + ), + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, + auto_commit_interval_ms: Annotated[ + int, + Doc( + """ + Milliseconds between automatic + offset commits, if `auto_commit` is `True`.""", + ), + ] = 5 * 1000, + check_crcs: Annotated[ + bool, + Doc( + """ + Automatically check the CRC32 of the records + consumed. This ensures no on-the-wire or on-disk corruption to + the messages occurred. This check adds some overhead, so it may + be disabled in cases seeking extreme performance. + """, + ), + ] = True, + partition_assignment_strategy: Annotated[ + Sequence["AbstractPartitionAssignor"], + Doc( + """ + List of objects to use to + distribute partition ownership amongst consumer instances when + group management is used. This preference is implicit in the order + of the strategies in the list. When assignment strategy changes: + to support a change to the assignment strategy, new versions must + enable support both for the old assignment strategy and the new + one. The coordinator will choose the old assignment strategy until + all members have been updated. Then it will choose the new + strategy. + """, + ), + ] = (RoundRobinPartitionAssignor,), + max_poll_interval_ms: Annotated[ + int, + Doc( + """ + Maximum allowed time between calls to + consume messages in batches. If this interval + is exceeded the consumer is considered failed and the group will + rebalance in order to reassign the partitions to another consumer + group member. If API methods block waiting for messages, that time + does not count against this timeout. + """, + ), + ] = 5 * 60 * 1000, + rebalance_timeout_ms: Annotated[ + int | None, + Doc( + """ + The maximum time server will wait for this + consumer to rejoin the group in a case of rebalance. In Java client + this behaviour is bound to `max.poll.interval.ms` configuration, + but as ``aiokafka`` will rejoin the group in the background, we + decouple this setting to allow finer tuning by users that use + `ConsumerRebalanceListener` to delay rebalacing. Defaults + to ``session_timeout_ms`` + """, + ), + ] = None, + session_timeout_ms: Annotated[ + int, + Doc( + """ + Client group session and failure detection + timeout. The consumer sends periodic heartbeats + (`heartbeat.interval.ms`) to indicate its liveness to the broker. + If no hearts are received by the broker for a group member within + the session timeout, the broker will remove the consumer from the + group and trigger a rebalance. The allowed range is configured with + the **broker** configuration properties + `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. + """, + ), + ] = 10 * 1000, + heartbeat_interval_ms: Annotated[ + int, + Doc( + """ + The expected time in milliseconds + between heartbeats to the consumer coordinator when using + Kafka's group management feature. Heartbeats are used to ensure + that the consumer's session stays active and to facilitate + rebalancing when new consumers join or leave the group. The + value must be set lower than `session_timeout_ms`, but typically + should be set no higher than 1/3 of that value. It can be + adjusted even lower to control the expected time for normal + rebalances. + """, + ), + ] = 3 * 1000, + consumer_timeout_ms: Annotated[ + int, + Doc( + """ + Maximum wait timeout for background fetching + routine. Mostly defines how fast the system will see rebalance and + request new data for new partitions. + """, + ), + ] = 200, + max_poll_records: Annotated[ + int | None, + Doc( + """ + The maximum number of records returned in a + single call by batch consumer. Has no limit by default. + """, + ), + ] = None, + exclude_internal_topics: Annotated[ + bool, + Doc( + """ + Whether records from internal topics + (such as offsets) should be exposed to the consumer. If set to True + the only way to receive records from an internal topic is + subscribing to it. + """, + ), + ] = True, + isolation_level: Annotated[ + Literal["read_uncommitted", "read_committed"], + Doc( + """ + Controls how to read messages written + transactionally. + + * `read_committed`, batch consumer will only return + transactional messages which have been committed. + + * `read_uncommitted` (the default), batch consumer will + return all messages, even transactional messages which have been + aborted. + + Non-transactional messages will be returned unconditionally in + either mode. + + Messages will always be returned in offset order. Hence, in + `read_committed` mode, batch consumer will only return + messages up to the last stable offset (LSO), which is the one less + than the offset of the first open transaction. In particular any + messages appearing after messages belonging to ongoing transactions + will be withheld until the relevant transaction has been completed. + As a result, `read_committed` consumers will not be able to read up + to the high watermark when there are in flight transactions. + Further, when in `read_committed` the seek_to_end method will + return the LSO. See method docs below. + """, + ), + ] = "read_uncommitted", + batch_timeout_ms: Annotated[ + int, + Doc( + """ + Milliseconds spent waiting if + data is not available in the buffer. If 0, returns immediately + with any records that are available currently in the buffer, + else returns empty. + """, + ), + ] = 200, + max_records: Annotated[ + int | None, + Doc("Number of messages to consume as one batch."), + ] = None, + listener: Annotated[ + Optional["ConsumerRebalanceListener"], + Doc( + """ + Optionally include listener + callback, which will be called before and after each rebalance + operation. + As part of group management, the consumer will keep track of + the list of consumers that belong to a particular group and + will trigger a rebalance operation if one of the following + events trigger: + + * Number of partitions change for any of the subscribed topics + * Topic is created or deleted + * An existing member of the consumer group dies + * A new member is added to the consumer group + + When any of these events are triggered, the provided listener + will be invoked first to indicate that the consumer's + assignment has been revoked, and then again when the new + assignment has been received. Note that this listener will + immediately override any listener set in a previous call + to subscribe. It is guaranteed, however, that the partitions + revoked/assigned + through this interface are from topics subscribed in this call. + """, + ), + ] = None, + pattern: Annotated[ + str | None, + Doc( + """ + Pattern to match available topics. You must provide either topics or pattern, but not both. + """, + ), + ] = None, + partitions: Annotated[ + Iterable["TopicPartition"] | None, + Doc( + """ + A topic and partition tuple. You can't use 'topics' and 'partitions' in the same time. + """, + ), + ] = (), + # broker args + dependencies: Annotated[ + Iterable["Dependant"], + Doc("Dependencies list (`[Dependant(),]`) to apply to the subscriber."), + ] = (), + parser: Annotated[ + Optional["CustomCallable"], + Doc("Parser to map original **ConsumerRecord** object to FastStream one."), + ] = None, + decoder: Annotated[ + Optional["CustomCallable"], + Doc("Function to decode FastStream msg bytes body to python objects."), + ] = None, + middlewares: Annotated[ + Sequence["SubscriberMiddleware[KafkaMessage]"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), + no_ack: Annotated[ + bool, + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), + deprecated( + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" + ), + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: Annotated[ + bool, + Doc( + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", + ), + ] = False, + # AsyncAPI args + title: Annotated[ + str | None, + Doc("AsyncAPI subscriber object title."), + ] = None, + description: Annotated[ + str | None, + Doc( + "AsyncAPI subscriber object description. " + "Uses decorated docstring as default.", + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = True, + max_workers: Annotated[ + int, + Doc("Number of workers to process messages concurrently."), + ] = 1, + ) -> None: + super().__init__( + call, + *topics, + publishers=publishers, + max_workers=max_workers, + group_id=group_id, + key_deserializer=key_deserializer, + value_deserializer=value_deserializer, + fetch_max_wait_ms=fetch_max_wait_ms, + fetch_max_bytes=fetch_max_bytes, + fetch_min_bytes=fetch_min_bytes, + max_partition_fetch_bytes=max_partition_fetch_bytes, + auto_offset_reset=auto_offset_reset, + auto_commit=auto_commit, + auto_commit_interval_ms=auto_commit_interval_ms, + check_crcs=check_crcs, + partition_assignment_strategy=partition_assignment_strategy, + max_poll_interval_ms=max_poll_interval_ms, + rebalance_timeout_ms=rebalance_timeout_ms, + session_timeout_ms=session_timeout_ms, + heartbeat_interval_ms=heartbeat_interval_ms, + consumer_timeout_ms=consumer_timeout_ms, + max_poll_records=max_poll_records, + exclude_internal_topics=exclude_internal_topics, + isolation_level=isolation_level, + max_records=max_records, + batch_timeout_ms=batch_timeout_ms, + batch=batch, + listener=listener, + pattern=pattern, + partitions=partitions, + # basic args + dependencies=dependencies, + parser=parser, + decoder=decoder, + middlewares=middlewares, + no_reply=no_reply, + ack_policy=ack_policy, + no_ack=no_ack, + # AsyncAPI args + title=title, + description=description, + include_in_schema=include_in_schema, + ) + + +class KafkaRouter( + KafkaRegistrator, + BrokerRouter[ + Union[ + "ConsumerRecord", + tuple["ConsumerRecord", ...], + ] + ], +): + """Includable to KafkaBroker router.""" + + def __init__( + self, + prefix: Annotated[ + str, + Doc("String prefix to add to all subscribers queues."), + ] = "", + handlers: Annotated[ + Iterable[KafkaRoute], + Doc("Route object to include."), + ] = (), + *, + dependencies: Annotated[ + Iterable["Dependant"], + Doc( + "Dependencies list (`[Dependant(),]`) to apply to all routers' publishers/subscribers.", + ), + ] = (), + middlewares: Annotated[ + Sequence[ + Union[ + "BrokerMiddleware[ConsumerRecord]", + "BrokerMiddleware[tuple[ConsumerRecord, ...]]", + ] + ], + Doc("Router middlewares to apply to all routers' publishers/subscribers."), + ] = (), + routers: Annotated[ + Sequence["Registrator[ConsumerRecord]"], + Doc("Routers to apply to broker."), + ] = (), + parser: Annotated[ + Optional["CustomCallable"], + Doc("Parser to map original **ConsumerRecord** object to FastStream one."), + ] = None, + decoder: Annotated[ + Optional["CustomCallable"], + Doc("Function to decode FastStream msg bytes body to python objects."), + ] = None, + include_in_schema: Annotated[ + bool | None, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = None, + ) -> None: + super().__init__( + handlers=handlers, + config=KafkaBrokerConfig( + broker_middlewares=middlewares, + broker_dependencies=dependencies, + broker_parser=parser, + broker_decoder=decoder, + include_in_schema=include_in_schema, + prefix=prefix, + ), + routers=routers, + ) diff --git a/faststream/kafka/configs/__init__.py b/faststream/kafka/configs/__init__.py new file mode 100644 index 0000000000..1794a6ebba --- /dev/null +++ b/faststream/kafka/configs/__init__.py @@ -0,0 +1,5 @@ +from .broker import KafkaBrokerConfig + +__all__ = ( + "KafkaBrokerConfig", +) diff --git a/faststream/kafka/configs/broker.py b/faststream/kafka/configs/broker.py new file mode 100644 index 0000000000..d0179342f4 --- /dev/null +++ b/faststream/kafka/configs/broker.py @@ -0,0 +1,61 @@ +from collections.abc import Callable +from dataclasses import dataclass, field +from functools import partial +from typing import Any, Optional + +import aiokafka +import aiokafka.admin + +from faststream.__about__ import SERVICE_NAME +from faststream._internal.configs import BrokerConfig +from faststream._internal.utils.data import filter_by_dict +from faststream.exceptions import IncorrectState +from faststream.kafka.publisher.producer import ( + AioKafkaFastProducer, + FakeAioKafkaFastProducer, +) +from faststream.kafka.schemas.params import ( + AdminClientConnectionParams, + ConsumerConnectionParams, +) + + +@dataclass(kw_only=True) +class KafkaBrokerConfig(BrokerConfig): + producer: "AioKafkaFastProducer" = field(default_factory=FakeAioKafkaFastProducer) + builder: Callable[..., aiokafka.AIOKafkaConsumer] = lambda: None + + client_id: str | None = SERVICE_NAME + + _admin_client: Optional["aiokafka.admin.client.AIOKafkaAdminClient"] = None + + @property + def admin_client(self) -> "aiokafka.admin.client.AIOKafkaAdminClient": + if self._admin_client is None: + msg = "Admin client is not initialized. Call connect() first." + raise IncorrectState(msg) + + return self._admin_client + + async def connect(self, **connection_kwargs: Any) -> "None": + producer = aiokafka.AIOKafkaProducer(**connection_kwargs) + await self.producer.connect(producer, serializer=self.fd_config._serializer) + + admin_options, _ = filter_by_dict( + AdminClientConnectionParams, connection_kwargs + ) + + self._admin_client = aiokafka.admin.client.AIOKafkaAdminClient(**admin_options) + await self._admin_client.start() + + consumer_options, _ = filter_by_dict( + ConsumerConnectionParams, connection_kwargs + ) + self.builder = partial(aiokafka.AIOKafkaConsumer, **consumer_options) + + async def disconnect(self) -> "None": + if self._admin_client is not None: + await self._admin_client.close() + self._admin_client = None + + await self.producer.disconnect() diff --git a/faststream/kafka/exceptions.py b/faststream/kafka/exceptions.py index bf51f6a401..443d2cfdc6 100644 --- a/faststream/kafka/exceptions.py +++ b/faststream/kafka/exceptions.py @@ -9,6 +9,6 @@ def __init__(self, message_position: int) -> None: def __str__(self) -> str: return ( - f"The batch buffer is full. The position of the message" + "The batch buffer is full. The position of the message" f" in the transferred collection at which the overflow occurred: {self.message_position}" ) diff --git a/faststream/kafka/fastapi/__init__.py b/faststream/kafka/fastapi/__init__.py index 88c88f215f..9fda6d07d3 100644 --- a/faststream/kafka/fastapi/__init__.py +++ b/faststream/kafka/fastapi/__init__.py @@ -1,11 +1,12 @@ -from typing_extensions import Annotated +from typing import Annotated -from faststream.broker.fastapi.context import Context, ContextRepo, Logger +from faststream._internal.fastapi.context import Context, ContextRepo, Logger from faststream.kafka.broker import KafkaBroker as KB -from faststream.kafka.fastapi.fastapi import KafkaRouter from faststream.kafka.message import KafkaMessage as KM from faststream.kafka.publisher.producer import AioKafkaFastProducer +from .fastapi import KafkaRouter + __all__ = ( "Context", "ContextRepo", diff --git a/faststream/kafka/fastapi/fastapi.py b/faststream/kafka/fastapi/fastapi.py index fc3cb682c2..ad87ae65b9 100644 --- a/faststream/kafka/fastapi/fastapi.py +++ b/faststream/kafka/fastapi/fastapi.py @@ -1,16 +1,11 @@ import logging +from collections.abc import Callable, Iterable, Sequence from typing import ( TYPE_CHECKING, + Annotated, Any, - Callable, - Dict, - Iterable, - List, Literal, Optional, - Sequence, - Tuple, - Type, TypeVar, Union, cast, @@ -26,13 +21,13 @@ from fastapi.utils import generate_unique_id from starlette.responses import JSONResponse, Response from starlette.routing import BaseRoute -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import Doc, deprecated, override from faststream.__about__ import SERVICE_NAME -from faststream.broker.fastapi.router import StreamRouter -from faststream.broker.utils import default_filter +from faststream._internal.constants import EMPTY +from faststream._internal.fastapi.router import StreamRouter from faststream.kafka.broker.broker import KafkaBroker as KB -from faststream.types import EMPTY +from faststream.middlewares import AckPolicy if TYPE_CHECKING: from asyncio import AbstractEventLoop @@ -45,32 +40,31 @@ from fastapi.types import IncEx from starlette.types import ASGIApp, Lifespan - from faststream.asyncapi import schema as asyncapi - from faststream.broker.types import ( + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, - Filter, PublisherMiddleware, SubscriberMiddleware, ) from faststream.kafka.message import KafkaMessage - from faststream.kafka.publisher.asyncapi import ( - AsyncAPIBatchPublisher, - AsyncAPIDefaultPublisher, + from faststream.kafka.publisher.specification import ( + SpecificationBatchPublisher, + SpecificationDefaultPublisher, ) - from faststream.kafka.subscriber.asyncapi import ( - AsyncAPIBatchSubscriber, - AsyncAPIConcurrentBetweenPartitionsSubscriber, - AsyncAPIConcurrentDefaultSubscriber, - AsyncAPIDefaultSubscriber, + from faststream.kafka.subscriber.specification import ( + SpecificationBatchSubscriber, + SpecificationConcurrentBetweenPartitionsSubscriber, + SpecificationConcurrentDefaultSubscriber, + SpecificationDefaultSubscriber, ) from faststream.security import BaseSecurity - from faststream.types import AnyDict, LoggerProto + from faststream.specification.schema.extra import Tag, TagDict Partition = TypeVar("Partition") -class KafkaRouter(StreamRouter[Union[ConsumerRecord, Tuple[ConsumerRecord, ...]]]): +class KafkaRouter(StreamRouter[ConsumerRecord | tuple[ConsumerRecord, ...]]): """A class to represent a Kafka router.""" broker_class = KB @@ -79,7 +73,7 @@ class KafkaRouter(StreamRouter[Union[ConsumerRecord, Tuple[ConsumerRecord, ...]] def __init__( self, bootstrap_servers: Annotated[ - Union[str, Iterable[str]], + str | Iterable[str], Doc( """ A `host[:port]` string (or list of `host[:port]` strings) that the consumer should contact to bootstrap @@ -88,7 +82,7 @@ def __init__( This does not have to be the full node list. It just needs to have at least one broker that will respond to a Metadata API Request. Default port is 9092. - """ + """, ), ] = "localhost", *, @@ -109,7 +103,7 @@ def __init__( which we force a refresh of metadata even if we haven't seen any partition leadership changes to proactively discover any new brokers or partitions. - """ + """, ), ] = 5 * 60 * 1000, connections_max_idle_ms: Annotated[ @@ -119,18 +113,18 @@ def __init__( Close idle connections after the number of milliseconds specified by this config. Specifying `None` will disable idle checks. - """ + """, ), ] = 9 * 60 * 1000, sasl_kerberos_service_name: str = "kafka", - sasl_kerberos_domain_name: Optional[str] = None, + sasl_kerberos_domain_name: str | None = None, sasl_oauth_token_provider: Annotated[ Optional["AbstractTokenProvider"], Doc("OAuthBearer token provider instance."), ] = None, loop: Optional["AbstractEventLoop"] = None, client_id: Annotated[ - Optional[str], + str | None, Doc( """ A name for this client. This string is passed in @@ -138,12 +132,12 @@ def __init__( server-side log entries that correspond to this client. Also submitted to :class:`~.consumer.group_coordinator.GroupCoordinator` for logging with respect to consumer group administration. - """ + """, ), ] = SERVICE_NAME, # publisher args acks: Annotated[ - Union[Literal[0, 1, -1, "all"], object], + Literal[0, 1, -1, "all"] | object, Doc( """ One of ``0``, ``1``, ``all``. The number of acknowledgments @@ -170,26 +164,26 @@ def __init__( If unset, defaults to ``acks=1``. If `enable_idempotence` is :data:`True` defaults to ``acks=all``. - """ + """, ), ] = _missing, key_serializer: Annotated[ - Optional[Callable[[Any], bytes]], + Callable[[Any], bytes] | None, Doc("Used to convert user-supplied keys to bytes."), ] = None, value_serializer: Annotated[ - Optional[Callable[[Any], bytes]], + Callable[[Any], bytes] | None, Doc("used to convert user-supplied message values to bytes."), ] = None, compression_type: Annotated[ - Optional[Literal["gzip", "snappy", "lz4", "zstd"]], + Literal["gzip", "snappy", "lz4", "zstd"] | None, Doc( """ The compression type for all data generated bythe producer. Compression is of full batches of data, so the efficacy of batching will also impact the compression ratio (more batching means better compression). - """ + """, ), ] = None, max_batch_size: Annotated[ @@ -198,12 +192,12 @@ def __init__( """ Maximum size of buffered data per partition. After this amount `send` coroutine will block until batch is drained. - """ + """, ), ] = 16 * 1024, partitioner: Annotated[ Callable[ - [bytes, List[Partition], List[Partition]], + [bytes, list[Partition], list[Partition]], Partition, ], Doc( @@ -216,7 +210,7 @@ def __init__( messages with the same key are assigned to the same partition. When a key is :data:`None`, the message is delivered to a random partition (filtered to partitions with available leaders only, if possible). - """ + """, ), ] = DefaultPartitioner(), max_request_size: Annotated[ @@ -228,7 +222,7 @@ def __init__( has its own cap on record size which may be different from this. This setting will limit the number of record batches the producer will send in a single request to avoid sending huge requests. - """ + """, ), ] = 1024 * 1024, linger_ms: Annotated[ @@ -243,7 +237,7 @@ def __init__( This setting accomplishes this by adding a small amount of artificial delay; that is, if first request is processed faster, than `linger_ms`, producer will wait ``linger_ms - process_time``. - """ + """, ), ] = 0, enable_idempotence: Annotated[ @@ -256,16 +250,16 @@ def __init__( etc., may write duplicates of the retried message in the stream. Note that enabling idempotence acks to set to ``all``. If it is not explicitly set by the user it will be chosen. - """ + """, ), ] = False, - transactional_id: Optional[str] = None, + transactional_id: str | None = None, transaction_timeout_ms: int = 60 * 1000, # broker base args graceful_timeout: Annotated[ - Optional[float], + float | None, Doc( - "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down." + "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down.", ), ] = 15.0, decoder: Annotated[ @@ -280,38 +274,40 @@ def __init__( Sequence[ Union[ "BrokerMiddleware[ConsumerRecord]", - "BrokerMiddleware[Tuple[ConsumerRecord, ...]]", + "BrokerMiddleware[tuple[ConsumerRecord, ...]]", ] ], Doc("Middlewares to apply to all broker publishers/subscribers."), ] = (), - # AsyncAPI args + # Specification args security: Annotated[ Optional["BaseSecurity"], Doc( - "Security options to connect broker and generate AsyncAPI server security information." + "Security options to connect broker and generate Specification server security information.", ), ] = None, - asyncapi_url: Annotated[ - Optional[str], - Doc("AsyncAPI hardcoded server addresses. Use `servers` if not specified."), + specification_url: Annotated[ + str | None, + Doc( + "Specification hardcoded server addresses. Use `servers` if not specified.", + ), ] = None, protocol: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol."), + str | None, + Doc("Specification server protocol."), ] = None, protocol_version: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol version."), + str | None, + Doc("Specification server protocol version."), ] = "auto", description: Annotated[ - Optional[str], - Doc("AsyncAPI server description."), - ] = None, - asyncapi_tags: Annotated[ - Optional[Iterable[Union["asyncapi.Tag", "asyncapi.TagDict"]]], - Doc("AsyncAPI server tags."), + str | None, + Doc("Specification server description."), ] = None, + specification_tags: Annotated[ + Iterable[Union["Tag", "TagDict"]], + Doc("Specification server tags."), + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], @@ -321,26 +317,18 @@ def __init__( int, Doc("Service messages log level."), ] = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - Doc("Default logger log format."), - ] = EMPTY, # StreamRouter options setup_state: Annotated[ bool, Doc( "Whether to add broker to app scope in lifespan. " - "You should disable this option at old ASGI servers." + "You should disable this option at old ASGI servers.", ), ] = True, schema_url: Annotated[ - Optional[str], + str | None, Doc( - "AsyncAPI schema url. You should set this option to `None` to disable AsyncAPI routes at all." + "Specification schema url. You should set this option to `None` to disable Specification routes at all.", ), ] = "/asyncapi", # FastAPI args @@ -349,7 +337,7 @@ def __init__( Doc("An optional path prefix for the router."), ] = "", tags: Annotated[ - Optional[List[Union[str, "Enum"]]], + list[Union[str, "Enum"]] | None, Doc( """ A list of tags to be applied to all the *path operations* in this @@ -359,11 +347,11 @@ def __init__( Read more about it in the [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). - """ + """, ), ] = None, dependencies: Annotated[ - Optional[Sequence["params.Depends"]], + Sequence["params.Depends"] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to all the @@ -371,22 +359,22 @@ def __init__( Read more about it in the [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). - """ + """, ), ] = None, default_response_class: Annotated[ - Type["Response"], + type["Response"], Doc( """ The default response class to be used. Read more in the [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). - """ + """, ), ] = Default(JSONResponse), responses: Annotated[ - Optional[Dict[Union[int, str], "AnyDict"]], + dict[int | str, "AnyDict"] | None, Doc( """ Additional responses to be shown in OpenAPI. @@ -398,11 +386,11 @@ def __init__( And in the [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). - """ + """, ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + list[BaseRoute] | None, Doc( """ OpenAPI callbacks that should apply to all *path operations* in this @@ -412,11 +400,11 @@ def __init__( Read more about it in the [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). - """ + """, ), ] = None, routes: Annotated[ - Optional[List[BaseRoute]], + list[BaseRoute] | None, Doc( """ **Note**: you probably shouldn't use this parameter, it is inherited @@ -425,7 +413,7 @@ def __init__( --- A list of routes to serve incoming HTTP and WebSocket requests. - """ + """, ), deprecated( """ @@ -434,7 +422,7 @@ def __init__( In FastAPI, you normally would use the *path operation methods*, like `router.get()`, `router.post()`, etc. - """ + """, ), ] = None, redirect_slashes: Annotated[ @@ -443,7 +431,7 @@ def __init__( """ Whether to detect and redirect slashes in URLs when the client doesn't use the same format. - """ + """, ), ] = True, default: Annotated[ @@ -452,33 +440,33 @@ def __init__( """ Default function handler for this router. Used to handle 404 Not Found errors. - """ + """, ), ] = None, dependency_overrides_provider: Annotated[ - Optional[Any], + Any | None, Doc( """ Only used internally by FastAPI to handle dependency overrides. You shouldn't need to use it. It normally points to the `FastAPI` app object. - """ + """, ), ] = None, route_class: Annotated[ - Type["APIRoute"], + type["APIRoute"], Doc( """ Custom route (*path operation*) class to be used by this router. Read more about it in the [FastAPI docs for Custom Request and APIRoute class](https://fastapi.tiangolo.com/how-to/custom-request-and-route/#custom-apiroute-class-in-a-router). - """ + """, ), ] = APIRoute, on_startup: Annotated[ - Optional[Sequence[Callable[[], Any]]], + Sequence[Callable[[], Any]] | None, Doc( """ A list of startup event handler functions. @@ -486,11 +474,11 @@ def __init__( You should instead use the `lifespan` handlers. Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ + """, ), ] = None, on_shutdown: Annotated[ - Optional[Sequence[Callable[[], Any]]], + Sequence[Callable[[], Any]] | None, Doc( """ A list of shutdown event handler functions. @@ -499,7 +487,7 @@ def __init__( Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ + """, ), ] = None, lifespan: Annotated[ @@ -511,11 +499,11 @@ def __init__( Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ + """, ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark all *path operations* in this router as deprecated. @@ -524,7 +512,7 @@ def __init__( Read more about it in the [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). - """ + """, ), ] = None, include_in_schema: Annotated[ @@ -538,7 +526,7 @@ def __init__( Read more about it in the [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). - """ + """, ), ] = True, generate_unique_id_function: Annotated[ @@ -553,7 +541,7 @@ def __init__( Read more about it in the [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). - """ + """, ), ] = Default(generate_unique_id), ) -> None: @@ -591,14 +579,13 @@ def __init__( # Logging args logger=logger, log_level=log_level, - log_fmt=log_fmt, - # AsyncAPI args + # Specification args security=security, protocol=protocol, description=description, protocol_version=protocol_version, - asyncapi_tags=asyncapi_tags, - asyncapi_url=asyncapi_url, + specification_tags=specification_tags, + specification_url=specification_url, # FastAPI args prefix=prefix, tags=tags, @@ -624,28 +611,28 @@ def subscriber( self, *topics: Annotated[str, Doc("Kafka topics to consume messages from.")], group_id: Annotated[ - Optional[str], + str | None, Doc( """ Name of the consumer group to join for dynamic partition assignment (if enabled), and to use for fetching and committing offsets. If `None`, auto-partition assignment (via group coordinator) and offset commits are disabled. - """ + """, ), ] = None, key_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "key and returns a deserialized one." + "key and returns a deserialized one.", ), ] = None, value_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "value and returns a deserialized value." + "value and returns a deserialized value.", ), ] = None, fetch_max_bytes: Annotated[ @@ -660,7 +647,7 @@ def subscriber( performs fetches to multiple brokers in parallel so memory usage will depend on the number of brokers containing partitions for the topic. - """ + """, ), ] = 50 * 1024 * 1024, fetch_min_bytes: Annotated[ @@ -670,7 +657,7 @@ def subscriber( Minimum amount of data the server should return for a fetch request, otherwise wait up to `fetch_max_wait_ms` for more data to accumulate. - """ + """, ), ] = 1, fetch_max_wait_ms: Annotated[ @@ -681,7 +668,7 @@ def subscriber( the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by `fetch_min_bytes`. - """ + """, ), ] = 500, max_partition_fetch_bytes: Annotated[ @@ -696,7 +683,7 @@ def subscriber( send messages larger than the consumer can fetch. If that happens, the consumer can get stuck trying to fetch a large message on a certain partition. - """ + """, ), ] = 1 * 1024 * 1024, auto_offset_reset: Annotated[ @@ -708,7 +695,7 @@ def subscriber( * `earliest` will move to the oldest available message * `latest` will move to the most recent * `none` will raise an exception so you can handle this case - """ + """, ), ] = "latest", auto_commit: Annotated[ @@ -717,15 +704,21 @@ def subscriber( """ If `True` the consumer's offset will be periodically committed in the background. - """ + """, ), - ] = True, + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, auto_commit_interval_ms: Annotated[ int, Doc( """ Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" + offset commits, if `auto_commit` is `True`.""", ), ] = 5 * 1000, check_crcs: Annotated[ @@ -736,7 +729,7 @@ def subscriber( consumed. This ensures no on-the-wire or on-disk corruption to the messages occurred. This check adds some overhead, so it may be disabled in cases seeking extreme performance. - """ + """, ), ] = True, partition_assignment_strategy: Annotated[ @@ -752,7 +745,7 @@ def subscriber( one. The coordinator will choose the old assignment strategy until all members have been updated. Then it will choose the new strategy. - """ + """, ), ] = (RoundRobinPartitionAssignor,), max_poll_interval_ms: Annotated[ @@ -765,11 +758,11 @@ def subscriber( rebalance in order to reassign the partitions to another consumer group member. If API methods block waiting for messages, that time does not count against this timeout. - """ + """, ), ] = 5 * 60 * 1000, rebalance_timeout_ms: Annotated[ - Optional[int], + int | None, Doc( """ The maximum time server will wait for this @@ -779,7 +772,7 @@ def subscriber( decouple this setting to allow finer tuning by users that use `ConsumerRebalanceListener` to delay rebalacing. Defaults to ``session_timeout_ms`` - """ + """, ), ] = None, session_timeout_ms: Annotated[ @@ -794,7 +787,7 @@ def subscriber( group and trigger a rebalance. The allowed range is configured with the **broker** configuration properties `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ + """, ), ] = 10 * 1000, heartbeat_interval_ms: Annotated[ @@ -810,7 +803,7 @@ def subscriber( should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances. - """ + """, ), ] = 3 * 1000, consumer_timeout_ms: Annotated[ @@ -820,16 +813,16 @@ def subscriber( Maximum wait timeout for background fetching routine. Mostly defines how fast the system will see rebalance and request new data for new partitions. - """ + """, ), ] = 200, max_poll_records: Annotated[ - Optional[int], + int | None, Doc( """ The maximum number of records returned in a single call by batch consumer. Has no limit by default. - """ + """, ), ] = None, exclude_internal_topics: Annotated[ @@ -840,7 +833,7 @@ def subscriber( (such as offsets) should be exposed to the consumer. If set to True the only way to receive records from an internal topic is subscribing to it. - """ + """, ), ] = True, isolation_level: Annotated[ @@ -870,7 +863,7 @@ def subscriber( to the high watermark when there are in flight transactions. Further, when in `read_committed` the seek_to_end method will return the LSO. See method docs below. - """ + """, ), ] = "read_uncommitted", batch_timeout_ms: Annotated[ @@ -881,11 +874,11 @@ def subscriber( data is not available in the buffer. If 0, returns immediately with any records that are available currently in the buffer, else returns empty. - """ + """, ), ] = 200, max_records: Annotated[ - Optional[int], + int | None, Doc("Number of messages to consume as one batch."), ] = None, batch: Annotated[ @@ -917,15 +910,15 @@ def subscriber( to subscribe. It is guaranteed, however, that the partitions revoked/assigned through this interface are from topics subscribed in this call. - """ + """, ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ Pattern to match available topics. You must provide either topics or pattern, but not both. - """ + """, ), ] = None, partitions: Annotated[ @@ -934,7 +927,7 @@ def subscriber( """ An explicit partitions list to assign. You can't use 'topics' and 'partitions' in the same time. - """ + """, ), ] = (), # broker args @@ -952,48 +945,42 @@ def subscriber( ] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), no_ack: Annotated[ bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), + deprecated( + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" + ), + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, - # AsyncAPI information + # Specification information title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), + str | None, + Doc("Specification subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Specification subscriber object description. " + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, # FastAPI args response_model: Annotated[ @@ -1027,7 +1014,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). - """ + """, ), ] = Default(None), response_model_include: Annotated[ @@ -1039,7 +1026,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_exclude: Annotated[ @@ -1051,7 +1038,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_by_alias: Annotated[ @@ -1063,7 +1050,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = True, response_model_exclude_unset: Annotated[ @@ -1081,7 +1068,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_defaults: Annotated[ @@ -1098,7 +1085,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_none: Annotated[ @@ -1115,38 +1102,38 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). - """ + """, ), ] = False, - ) -> "AsyncAPIDefaultSubscriber": ... + ) -> "SpecificationDefaultSubscriber": ... @overload def subscriber( self, *topics: Annotated[str, Doc("Kafka topics to consume messages from.")], group_id: Annotated[ - Optional[str], + str | None, Doc( """ Name of the consumer group to join for dynamic partition assignment (if enabled), and to use for fetching and committing offsets. If `None`, auto-partition assignment (via group coordinator) and offset commits are disabled. - """ + """, ), ] = None, key_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "key and returns a deserialized one." + "key and returns a deserialized one.", ), ] = None, value_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "value and returns a deserialized value." + "value and returns a deserialized value.", ), ] = None, fetch_max_bytes: Annotated[ @@ -1161,7 +1148,7 @@ def subscriber( performs fetches to multiple brokers in parallel so memory usage will depend on the number of brokers containing partitions for the topic. - """ + """, ), ] = 50 * 1024 * 1024, fetch_min_bytes: Annotated[ @@ -1171,7 +1158,7 @@ def subscriber( Minimum amount of data the server should return for a fetch request, otherwise wait up to `fetch_max_wait_ms` for more data to accumulate. - """ + """, ), ] = 1, fetch_max_wait_ms: Annotated[ @@ -1182,7 +1169,7 @@ def subscriber( the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by `fetch_min_bytes`. - """ + """, ), ] = 500, max_partition_fetch_bytes: Annotated[ @@ -1197,7 +1184,7 @@ def subscriber( send messages larger than the consumer can fetch. If that happens, the consumer can get stuck trying to fetch a large message on a certain partition. - """ + """, ), ] = 1 * 1024 * 1024, auto_offset_reset: Annotated[ @@ -1209,7 +1196,7 @@ def subscriber( * `earliest` will move to the oldest available message * `latest` will move to the most recent * `none` will raise an exception so you can handle this case - """ + """, ), ] = "latest", auto_commit: Annotated[ @@ -1218,15 +1205,21 @@ def subscriber( """ If `True` the consumer's offset will be periodically committed in the background. - """ + """, ), - ] = True, + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, auto_commit_interval_ms: Annotated[ int, Doc( """ Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" + offset commits, if `auto_commit` is `True`.""", ), ] = 5 * 1000, check_crcs: Annotated[ @@ -1237,7 +1230,7 @@ def subscriber( consumed. This ensures no on-the-wire or on-disk corruption to the messages occurred. This check adds some overhead, so it may be disabled in cases seeking extreme performance. - """ + """, ), ] = True, partition_assignment_strategy: Annotated[ @@ -1253,7 +1246,7 @@ def subscriber( one. The coordinator will choose the old assignment strategy until all members have been updated. Then it will choose the new strategy. - """ + """, ), ] = (RoundRobinPartitionAssignor,), max_poll_interval_ms: Annotated[ @@ -1266,11 +1259,11 @@ def subscriber( rebalance in order to reassign the partitions to another consumer group member. If API methods block waiting for messages, that time does not count against this timeout. - """ + """, ), ] = 5 * 60 * 1000, rebalance_timeout_ms: Annotated[ - Optional[int], + int | None, Doc( """ The maximum time server will wait for this @@ -1280,7 +1273,7 @@ def subscriber( decouple this setting to allow finer tuning by users that use `ConsumerRebalanceListener` to delay rebalacing. Defaults to ``session_timeout_ms`` - """ + """, ), ] = None, session_timeout_ms: Annotated[ @@ -1295,7 +1288,7 @@ def subscriber( group and trigger a rebalance. The allowed range is configured with the **broker** configuration properties `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ + """, ), ] = 10 * 1000, heartbeat_interval_ms: Annotated[ @@ -1311,7 +1304,7 @@ def subscriber( should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances. - """ + """, ), ] = 3 * 1000, consumer_timeout_ms: Annotated[ @@ -1321,16 +1314,16 @@ def subscriber( Maximum wait timeout for background fetching routine. Mostly defines how fast the system will see rebalance and request new data for new partitions. - """ + """, ), ] = 200, max_poll_records: Annotated[ - Optional[int], + int | None, Doc( """ The maximum number of records returned in a single call by batch consumer. Has no limit by default. - """ + """, ), ] = None, exclude_internal_topics: Annotated[ @@ -1341,7 +1334,7 @@ def subscriber( (such as offsets) should be exposed to the consumer. If set to True the only way to receive records from an internal topic is subscribing to it. - """ + """, ), ] = True, isolation_level: Annotated[ @@ -1371,7 +1364,7 @@ def subscriber( to the high watermark when there are in flight transactions. Further, when in `read_committed` the seek_to_end method will return the LSO. See method docs below. - """ + """, ), ] = "read_uncommitted", batch_timeout_ms: Annotated[ @@ -1382,11 +1375,11 @@ def subscriber( data is not available in the buffer. If 0, returns immediately with any records that are available currently in the buffer, else returns empty. - """ + """, ), ] = 200, max_records: Annotated[ - Optional[int], + int | None, Doc("Number of messages to consume as one batch."), ] = None, batch: Annotated[ @@ -1418,15 +1411,15 @@ def subscriber( to subscribe. It is guaranteed, however, that the partitions revoked/assigned through this interface are from topics subscribed in this call. - """ + """, ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ Pattern to match available topics. You must provide either topics or pattern, but not both. - """ + """, ), ] = None, partitions: Annotated[ @@ -1435,7 +1428,7 @@ def subscriber( """ An explicit partitions list to assign. You can't use 'topics' and 'partitions' in the same time. - """ + """, ), ] = (), # broker args @@ -1452,49 +1445,43 @@ def subscriber( Doc("Function to decode FastStream msg bytes body to python objects."), ] = None, middlewares: Annotated[ - Iterable["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), + Sequence["SubscriberMiddleware[KafkaMessage]"], deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), no_ack: Annotated[ bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), + deprecated( + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" + ), + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, - # AsyncAPI information + # Specification information title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), + str | None, + Doc("Specification subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Specification subscriber object description. " + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, # FastAPI args response_model: Annotated[ @@ -1528,7 +1515,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). - """ + """, ), ] = Default(None), response_model_include: Annotated[ @@ -1540,7 +1527,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_exclude: Annotated[ @@ -1552,7 +1539,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_by_alias: Annotated[ @@ -1564,7 +1551,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = True, response_model_exclude_unset: Annotated[ @@ -1582,7 +1569,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_defaults: Annotated[ @@ -1599,7 +1586,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_none: Annotated[ @@ -1616,38 +1603,38 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). - """ + """, ), ] = False, - ) -> "AsyncAPIBatchSubscriber": ... + ) -> "SpecificationBatchSubscriber": ... @overload def subscriber( self, *topics: Annotated[str, Doc("Kafka topics to consume messages from.")], group_id: Annotated[ - Optional[str], + str | None, Doc( """ Name of the consumer group to join for dynamic partition assignment (if enabled), and to use for fetching and committing offsets. If `None`, auto-partition assignment (via group coordinator) and offset commits are disabled. - """ + """, ), ] = None, key_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "key and returns a deserialized one." + "key and returns a deserialized one.", ), ] = None, value_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "value and returns a deserialized value." + "value and returns a deserialized value.", ), ] = None, fetch_max_bytes: Annotated[ @@ -1662,7 +1649,7 @@ def subscriber( performs fetches to multiple brokers in parallel so memory usage will depend on the number of brokers containing partitions for the topic. - """ + """, ), ] = 50 * 1024 * 1024, fetch_min_bytes: Annotated[ @@ -1672,7 +1659,7 @@ def subscriber( Minimum amount of data the server should return for a fetch request, otherwise wait up to `fetch_max_wait_ms` for more data to accumulate. - """ + """, ), ] = 1, fetch_max_wait_ms: Annotated[ @@ -1683,7 +1670,7 @@ def subscriber( the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by `fetch_min_bytes`. - """ + """, ), ] = 500, max_partition_fetch_bytes: Annotated[ @@ -1698,7 +1685,7 @@ def subscriber( send messages larger than the consumer can fetch. If that happens, the consumer can get stuck trying to fetch a large message on a certain partition. - """ + """, ), ] = 1 * 1024 * 1024, auto_offset_reset: Annotated[ @@ -1710,7 +1697,7 @@ def subscriber( * `earliest` will move to the oldest available message * `latest` will move to the most recent * `none` will raise an exception so you can handle this case - """ + """, ), ] = "latest", auto_commit: Annotated[ @@ -1719,15 +1706,21 @@ def subscriber( """ If `True` the consumer's offset will be periodically committed in the background. - """ + """, ), - ] = True, + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, auto_commit_interval_ms: Annotated[ int, Doc( """ Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" + offset commits, if `auto_commit` is `True`.""", ), ] = 5 * 1000, check_crcs: Annotated[ @@ -1738,7 +1731,7 @@ def subscriber( consumed. This ensures no on-the-wire or on-disk corruption to the messages occurred. This check adds some overhead, so it may be disabled in cases seeking extreme performance. - """ + """, ), ] = True, partition_assignment_strategy: Annotated[ @@ -1754,7 +1747,7 @@ def subscriber( one. The coordinator will choose the old assignment strategy until all members have been updated. Then it will choose the new strategy. - """ + """, ), ] = (RoundRobinPartitionAssignor,), max_poll_interval_ms: Annotated[ @@ -1767,11 +1760,11 @@ def subscriber( rebalance in order to reassign the partitions to another consumer group member. If API methods block waiting for messages, that time does not count against this timeout. - """ + """, ), ] = 5 * 60 * 1000, rebalance_timeout_ms: Annotated[ - Optional[int], + int | None, Doc( """ The maximum time server will wait for this @@ -1781,7 +1774,7 @@ def subscriber( decouple this setting to allow finer tuning by users that use `ConsumerRebalanceListener` to delay rebalacing. Defaults to ``session_timeout_ms`` - """ + """, ), ] = None, session_timeout_ms: Annotated[ @@ -1796,7 +1789,7 @@ def subscriber( group and trigger a rebalance. The allowed range is configured with the **broker** configuration properties `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ + """, ), ] = 10 * 1000, heartbeat_interval_ms: Annotated[ @@ -1812,7 +1805,7 @@ def subscriber( should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances. - """ + """, ), ] = 3 * 1000, consumer_timeout_ms: Annotated[ @@ -1822,16 +1815,16 @@ def subscriber( Maximum wait timeout for background fetching routine. Mostly defines how fast the system will see rebalance and request new data for new partitions. - """ + """, ), ] = 200, max_poll_records: Annotated[ - Optional[int], + int | None, Doc( """ The maximum number of records returned in a single call by batch consumer. Has no limit by default. - """ + """, ), ] = None, exclude_internal_topics: Annotated[ @@ -1842,7 +1835,7 @@ def subscriber( (such as offsets) should be exposed to the consumer. If set to True the only way to receive records from an internal topic is subscribing to it. - """ + """, ), ] = True, isolation_level: Annotated[ @@ -1872,7 +1865,7 @@ def subscriber( to the high watermark when there are in flight transactions. Further, when in `read_committed` the seek_to_end method will return the LSO. See method docs below. - """ + """, ), ] = "read_uncommitted", batch_timeout_ms: Annotated[ @@ -1883,11 +1876,11 @@ def subscriber( data is not available in the buffer. If 0, returns immediately with any records that are available currently in the buffer, else returns empty. - """ + """, ), ] = 200, max_records: Annotated[ - Optional[int], + int | None, Doc("Number of messages to consume as one batch."), ] = None, batch: Annotated[ @@ -1919,15 +1912,15 @@ def subscriber( to subscribe. It is guaranteed, however, that the partitions revoked/assigned through this interface are from topics subscribed in this call. - """ + """, ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ Pattern to match available topics. You must provide either topics or pattern, but not both. - """ + """, ), ] = None, partitions: Annotated[ @@ -1936,7 +1929,7 @@ def subscriber( """ An explicit partitions list to assign. You can't use 'topics' and 'partitions' in the same time. - """ + """, ), ] = (), # broker args @@ -1953,49 +1946,43 @@ def subscriber( Doc("Function to decode FastStream msg bytes body to python objects."), ] = None, middlewares: Annotated[ - Iterable["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), + Sequence["SubscriberMiddleware[KafkaMessage]"], deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), no_ack: Annotated[ bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), + deprecated( + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" + ), + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, - # AsyncAPI information + # Specification information title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), + str | None, + Doc("Specification subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Specification subscriber object description. " + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, # FastAPI args response_model: Annotated[ @@ -2029,7 +2016,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). - """ + """, ), ] = Default(None), response_model_include: Annotated[ @@ -2041,7 +2028,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_exclude: Annotated[ @@ -2053,7 +2040,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_by_alias: Annotated[ @@ -2065,7 +2052,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = True, response_model_exclude_unset: Annotated[ @@ -2083,7 +2070,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_defaults: Annotated[ @@ -2100,7 +2087,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_none: Annotated[ @@ -2117,12 +2104,12 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). - """ + """, ), ] = False, ) -> Union[ - "AsyncAPIBatchSubscriber", - "AsyncAPIDefaultSubscriber", + "SpecificationBatchSubscriber", + "SpecificationDefaultSubscriber", ]: ... @override @@ -2130,28 +2117,28 @@ def subscriber( self, *topics: Annotated[str, Doc("Kafka topics to consume messages from.")], group_id: Annotated[ - Optional[str], + str | None, Doc( """ Name of the consumer group to join for dynamic partition assignment (if enabled), and to use for fetching and committing offsets. If `None`, auto-partition assignment (via group coordinator) and offset commits are disabled. - """ + """, ), ] = None, key_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "key and returns a deserialized one." + "key and returns a deserialized one.", ), ] = None, value_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], + Callable[[bytes], Any] | None, Doc( "Any callable that takes a raw message `bytes` " - "value and returns a deserialized value." + "value and returns a deserialized value.", ), ] = None, fetch_max_bytes: Annotated[ @@ -2166,7 +2153,7 @@ def subscriber( performs fetches to multiple brokers in parallel so memory usage will depend on the number of brokers containing partitions for the topic. - """ + """, ), ] = 50 * 1024 * 1024, fetch_min_bytes: Annotated[ @@ -2176,7 +2163,7 @@ def subscriber( Minimum amount of data the server should return for a fetch request, otherwise wait up to `fetch_max_wait_ms` for more data to accumulate. - """ + """, ), ] = 1, fetch_max_wait_ms: Annotated[ @@ -2187,7 +2174,7 @@ def subscriber( the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by `fetch_min_bytes`. - """ + """, ), ] = 500, max_partition_fetch_bytes: Annotated[ @@ -2202,7 +2189,7 @@ def subscriber( send messages larger than the consumer can fetch. If that happens, the consumer can get stuck trying to fetch a large message on a certain partition. - """ + """, ), ] = 1 * 1024 * 1024, auto_offset_reset: Annotated[ @@ -2214,7 +2201,7 @@ def subscriber( * `earliest` will move to the oldest available message * `latest` will move to the most recent * `none` will raise an exception so you can handle this case - """ + """, ), ] = "latest", auto_commit: Annotated[ @@ -2223,15 +2210,21 @@ def subscriber( """ If `True` the consumer's offset will be periodically committed in the background. - """ + """, ), - ] = True, + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, auto_commit_interval_ms: Annotated[ int, Doc( """ Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" + offset commits, if `auto_commit` is `True`.""", ), ] = 5 * 1000, check_crcs: Annotated[ @@ -2242,7 +2235,7 @@ def subscriber( consumed. This ensures no on-the-wire or on-disk corruption to the messages occurred. This check adds some overhead, so it may be disabled in cases seeking extreme performance. - """ + """, ), ] = True, partition_assignment_strategy: Annotated[ @@ -2258,7 +2251,7 @@ def subscriber( one. The coordinator will choose the old assignment strategy until all members have been updated. Then it will choose the new strategy. - """ + """, ), ] = (RoundRobinPartitionAssignor,), max_poll_interval_ms: Annotated[ @@ -2271,11 +2264,11 @@ def subscriber( rebalance in order to reassign the partitions to another consumer group member. If API methods block waiting for messages, that time does not count against this timeout. - """ + """, ), ] = 5 * 60 * 1000, rebalance_timeout_ms: Annotated[ - Optional[int], + int | None, Doc( """ The maximum time server will wait for this @@ -2285,7 +2278,7 @@ def subscriber( decouple this setting to allow finer tuning by users that use `ConsumerRebalanceListener` to delay rebalacing. Defaults to ``session_timeout_ms`` - """ + """, ), ] = None, session_timeout_ms: Annotated[ @@ -2300,7 +2293,7 @@ def subscriber( group and trigger a rebalance. The allowed range is configured with the **broker** configuration properties `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ + """, ), ] = 10 * 1000, heartbeat_interval_ms: Annotated[ @@ -2316,7 +2309,7 @@ def subscriber( should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances. - """ + """, ), ] = 3 * 1000, consumer_timeout_ms: Annotated[ @@ -2326,16 +2319,16 @@ def subscriber( Maximum wait timeout for background fetching routine. Mostly defines how fast the system will see rebalance and request new data for new partitions. - """ + """, ), ] = 200, max_poll_records: Annotated[ - Optional[int], + int | None, Doc( """ The maximum number of records returned in a single call by batch consumer. Has no limit by default. - """ + """, ), ] = None, exclude_internal_topics: Annotated[ @@ -2346,7 +2339,7 @@ def subscriber( (such as offsets) should be exposed to the consumer. If set to True the only way to receive records from an internal topic is subscribing to it. - """ + """, ), ] = True, isolation_level: Annotated[ @@ -2376,7 +2369,7 @@ def subscriber( to the high watermark when there are in flight transactions. Further, when in `read_committed` the seek_to_end method will return the LSO. See method docs below. - """ + """, ), ] = "read_uncommitted", batch_timeout_ms: Annotated[ @@ -2387,11 +2380,11 @@ def subscriber( data is not available in the buffer. If 0, returns immediately with any records that are available currently in the buffer, else returns empty. - """ + """, ), ] = 200, max_records: Annotated[ - Optional[int], + int | None, Doc("Number of messages to consume as one batch."), ] = None, batch: Annotated[ @@ -2423,15 +2416,15 @@ def subscriber( to subscribe. It is guaranteed, however, that the partitions revoked/assigned through this interface are from topics subscribed in this call. - """ + """, ), ] = None, pattern: Annotated[ - Optional[str], + str | None, Doc( """ Pattern to match available topics. You must provide either topics or pattern, but not both. - """ + """, ), ] = None, partitions: Annotated[ @@ -2440,7 +2433,7 @@ def subscriber( """ An explicit partitions list to assign. You can't use 'topics' and 'partitions' in the same time. - """ + """, ), ] = (), # broker args @@ -2457,49 +2450,43 @@ def subscriber( Doc("Function to decode FastStream msg bytes body to python objects."), ] = None, middlewares: Annotated[ - Iterable["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), + Sequence["SubscriberMiddleware[KafkaMessage]"], deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), no_ack: Annotated[ bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), + deprecated( + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" + ), + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, - # AsyncAPI information + # Specification information title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), + str | None, + Doc("Specification subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Specification subscriber object description. " + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, # FastAPI args response_model: Annotated[ @@ -2533,7 +2520,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). - """ + """, ), ] = Default(None), response_model_include: Annotated[ @@ -2545,7 +2532,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_exclude: Annotated[ @@ -2557,7 +2544,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_by_alias: Annotated[ @@ -2569,7 +2556,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = True, response_model_exclude_unset: Annotated[ @@ -2587,7 +2574,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_defaults: Annotated[ @@ -2604,7 +2591,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_none: Annotated[ @@ -2621,7 +2608,7 @@ def subscriber( Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). - """ + """, ), ] = False, max_workers: Annotated[ @@ -2636,10 +2623,10 @@ def subscriber( ), ] = 1, ) -> Union[ - "AsyncAPIBatchSubscriber", - "AsyncAPIDefaultSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", - "AsyncAPIConcurrentBetweenPartitionsSubscriber", + "SpecificationBatchSubscriber", + "SpecificationDefaultSubscriber", + "SpecificationConcurrentDefaultSubscriber", + "SpecificationConcurrentBetweenPartitionsSubscriber", ]: subscriber = super().subscriber( *topics, @@ -2675,8 +2662,7 @@ def subscriber( parser=parser, decoder=decoder, middlewares=middlewares, - filter=filter, - retry=retry, + ack_policy=ack_policy, no_ack=no_ack, no_reply=no_reply, title=title, @@ -2693,17 +2679,14 @@ def subscriber( ) if batch: - return cast("AsyncAPIBatchSubscriber", subscriber) - else: - if max_workers > 1: - if not auto_commit: - return cast( - "AsyncAPIConcurrentBetweenPartitionsSubscriber", subscriber - ) - else: - return cast("AsyncAPIConcurrentDefaultSubscriber", subscriber) - else: - return cast("AsyncAPIDefaultSubscriber", subscriber) + return cast("SpecificationBatchSubscriber", subscriber) + if max_workers > 1: + if auto_commit: + return cast("SpecificationConcurrentDefaultSubscriber", subscriber) + return cast( + "SpecificationConcurrentBetweenPartitionsSubscriber", subscriber + ) + return cast("SpecificationDefaultSubscriber", subscriber) @overload # type: ignore[override] def publisher( @@ -2714,7 +2697,7 @@ def publisher( ], *, key: Annotated[ - Union[bytes, Any, None], + bytes | Any | None, Doc( """ A key to associate with the message. Can be used to @@ -2724,24 +2707,24 @@ def publisher( partition (but if key is `None`, partition is chosen randomly). Must be type `bytes`, or be serializable to bytes via configured `key_serializer`. - """ + """, ), ] = None, partition: Annotated[ - Optional[int], + int | None, Doc( """ Specify a partition. If not set, the partition will be selected using the configured `partitioner`. - """ + """, ), ] = None, headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc( "Message headers to store metainformation. " "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -2755,29 +2738,33 @@ def publisher( # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), + str | None, + Doc("Specification publisher object title."), ] = None, description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), + str | None, + Doc("Specification publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Specification publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, - ) -> "AsyncAPIDefaultPublisher": ... + ) -> "SpecificationDefaultPublisher": ... @overload def publisher( @@ -2788,7 +2775,7 @@ def publisher( ], *, key: Annotated[ - Union[bytes, Any, None], + bytes | Any | None, Doc( """ A key to associate with the message. Can be used to @@ -2798,24 +2785,24 @@ def publisher( partition (but if key is `None`, partition is chosen randomly). Must be type `bytes`, or be serializable to bytes via configured `key_serializer`. - """ + """, ), ] = None, partition: Annotated[ - Optional[int], + int | None, Doc( """ Specify a partition. If not set, the partition will be selected using the configured `partitioner`. - """ + """, ), ] = None, headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc( "Message headers to store metainformation. " "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -2829,29 +2816,33 @@ def publisher( # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), + str | None, + Doc("Specification publisher object title."), ] = None, description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), + str | None, + Doc("Specification publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Specification publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, - ) -> "AsyncAPIBatchPublisher": ... + ) -> "SpecificationBatchPublisher": ... @overload def publisher( @@ -2862,7 +2853,7 @@ def publisher( ], *, key: Annotated[ - Union[bytes, Any, None], + bytes | Any | None, Doc( """ A key to associate with the message. Can be used to @@ -2872,24 +2863,24 @@ def publisher( partition (but if key is `None`, partition is chosen randomly). Must be type `bytes`, or be serializable to bytes via configured `key_serializer`. - """ + """, ), ] = None, partition: Annotated[ - Optional[int], + int | None, Doc( """ Specify a partition. If not set, the partition will be selected using the configured `partitioner`. - """ + """, ), ] = None, headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc( "Message headers to store metainformation. " "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -2903,31 +2894,35 @@ def publisher( # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), + str | None, + Doc("Specification publisher object title."), ] = None, description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), + str | None, + Doc("Specification publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Specification publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", + "SpecificationBatchPublisher", + "SpecificationDefaultPublisher", ]: ... @override @@ -2939,7 +2934,7 @@ def publisher( ], *, key: Annotated[ - Union[bytes, Any, None], + bytes | Any | None, Doc( """ A key to associate with the message. Can be used to @@ -2949,24 +2944,24 @@ def publisher( partition (but if key is `None`, partition is chosen randomly). Must be type `bytes`, or be serializable to bytes via configured `key_serializer`. - """ + """, ), ] = None, partition: Annotated[ - Optional[int], + int | None, Doc( """ Specify a partition. If not set, the partition will be selected using the configured `partitioner`. - """ + """, ), ] = None, headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc( "Message headers to store metainformation. " "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -2980,31 +2975,35 @@ def publisher( # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), - # AsyncAPI args + # Specification args title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), + str | None, + Doc("Specification publisher object title."), ] = None, description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), + str | None, + Doc("Specification publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Specification publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), + Doc("Whetever to include operation in Specification schema or not."), ] = True, ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", + "SpecificationBatchPublisher", + "SpecificationDefaultPublisher", ]: return self.broker.publisher( topic=topic, @@ -3015,7 +3014,7 @@ def publisher( reply_to=reply_to, # broker options middlewares=middlewares, - # AsyncAPI options + # Specification options title=title, description=description, schema=schema, diff --git a/faststream/kafka/helpers/__init__.py b/faststream/kafka/helpers/__init__.py new file mode 100644 index 0000000000..93e6ef02af --- /dev/null +++ b/faststream/kafka/helpers/__init__.py @@ -0,0 +1,3 @@ +from .rebalance_listener import make_logging_listener + +__all__ = ("make_logging_listener",) diff --git a/faststream/kafka/helpers/rebalance_listener.py b/faststream/kafka/helpers/rebalance_listener.py new file mode 100644 index 0000000000..720e9bddee --- /dev/null +++ b/faststream/kafka/helpers/rebalance_listener.py @@ -0,0 +1,108 @@ +import asyncio +import logging +from typing import TYPE_CHECKING, Optional + +from aiokafka import ConsumerRebalanceListener + +from faststream._internal.utils.functions import call_or_await + +if TYPE_CHECKING: + from aiokafka import AIOKafkaConsumer, TopicPartition + + from faststream._internal.basic_types import AnyDict, LoggerProto + + +def make_logging_listener( + *, + consumer: "AIOKafkaConsumer", + logger: Optional["LoggerProto"], + log_extra: "AnyDict", + listener: Optional["ConsumerRebalanceListener"], +) -> Optional["ConsumerRebalanceListener"]: + if logger is None: + return listener + + logging_listener = _LoggingListener( + consumer=consumer, + logger=logger, + log_extra=log_extra, + ) + if listener is None: + return logging_listener + + return _LoggingListenerFacade( + logging_listener=logging_listener, + listener=listener, + ) + + +class _LoggingListener(ConsumerRebalanceListener): + _log_unassigned_consumer_delay_seconds = 60 * 2 + + def __init__( + self, + *, + consumer: "AIOKafkaConsumer", + logger: "LoggerProto", + log_extra: "AnyDict", + ) -> None: + self.consumer = consumer + self.logger = logger + self.log_extra = log_extra + + async def on_partitions_revoked(self, revoked: set["TopicPartition"]) -> None: + pass + + async def log_unassigned_consumer(self) -> None: + await asyncio.sleep(self._log_unassigned_consumer_delay_seconds) + self.logger.log( + logging.WARNING, + f"Consumer in group {self.consumer._group_id} has had no partition " + f"assignments for {self._log_unassigned_consumer_delay_seconds} seconds: " + f"topics {self.consumer._subscription.topics} may have fewer partitions " + f"than consumers.", + extra=self.log_extra, + ) + + async def on_partitions_assigned(self, assigned: set["TopicPartition"]) -> None: + self.logger.log( + logging.INFO, + f"Consumer {self.consumer._coordinator.member_id} assigned to partitions: " + f"{assigned}", + extra=self.log_extra, + ) + + if not assigned: + self.logger.log( + logging.WARNING, + f"Consumer in group {self.consumer._group_id} has no partition assignments - this " + f"could be temporary, e.g. during a rolling update. A separate warning will be logged if " + f"this condition persists for {self._log_unassigned_consumer_delay_seconds} seconds.", + extra=self.log_extra, + ) + + self._log_unassigned_consumer_task: asyncio.Task[None] | None = ( + asyncio.create_task(self.log_unassigned_consumer()) + ) + + elif self._log_unassigned_consumer_task: + self._log_unassigned_consumer_task.cancel() + self._log_unassigned_consumer_task = None + + +class _LoggingListenerFacade(ConsumerRebalanceListener): + def __init__( + self, + *, + logging_listener: _LoggingListener, + listener: ConsumerRebalanceListener, + ) -> None: + self.logging_listener = logging_listener + self.listener = listener + + async def on_partitions_revoked(self, revoked: set["TopicPartition"]) -> None: + await call_or_await(self.listener.on_partitions_revoked, revoked) + + async def on_partitions_assigned(self, assigned: set["TopicPartition"]) -> None: + await self.logging_listener.on_partitions_revoked(assigned) + await call_or_await(self.listener.on_partitions_assigned, assigned) diff --git a/faststream/kafka/listener.py b/faststream/kafka/listener.py index d70e6f1913..9858d42441 100644 --- a/faststream/kafka/listener.py +++ b/faststream/kafka/listener.py @@ -1,83 +1,110 @@ import asyncio import logging -from typing import TYPE_CHECKING, Optional, Set +from typing import TYPE_CHECKING, Optional from aiokafka import ConsumerRebalanceListener +from faststream._internal.utils.functions import call_or_await + if TYPE_CHECKING: from aiokafka import AIOKafkaConsumer, TopicPartition - from faststream.types import AnyDict, LoggerProto + from faststream._internal.basic_types import AnyDict, LoggerProto + + +def make_logging_listener( + *, + consumer: "AIOKafkaConsumer", + logger: Optional["LoggerProto"], + log_extra: "AnyDict", + listener: Optional["ConsumerRebalanceListener"], +) -> Optional["ConsumerRebalanceListener"]: + if logger is None: + return listener + logging_listener = _LoggingListener( + consumer=consumer, + logger=logger, + log_extra=log_extra, + ) + if listener is None: + return logging_listener -class LoggingListenerProxy(ConsumerRebalanceListener): # type: ignore[misc] - """Logs partition assignments and passes calls to user-supplied listener.""" + return _LoggingListenerFacade( + logging_listener=logging_listener, + listener=listener, + ) + +class _LoggingListener(ConsumerRebalanceListener): _log_unassigned_consumer_delay_seconds = 60 * 2 def __init__( self, + *, consumer: "AIOKafkaConsumer", - logger: Optional["LoggerProto"], - listener: Optional[ConsumerRebalanceListener], - ): + logger: "LoggerProto", + log_extra: "AnyDict", + ) -> None: self.consumer = consumer self.logger = logger - self.listener = listener - self._log_unassigned_consumer_task: Optional[asyncio.Task[None]] = None + self.log_extra = log_extra + + self._log_unassigned_consumer_task: asyncio.Task[None] | None = None + + async def on_partitions_revoked(self, revoked: set["TopicPartition"]) -> None: + pass async def log_unassigned_consumer(self) -> None: await asyncio.sleep(self._log_unassigned_consumer_delay_seconds) - self._log( + self.logger.log( logging.WARNING, f"Consumer in group {self.consumer._group_id} has had no partition " f"assignments for {self._log_unassigned_consumer_delay_seconds} seconds: " f"topics {self.consumer._subscription.topics} may have fewer partitions " f"than consumers.", + extra=self.log_extra, ) - async def on_partitions_revoked(self, revoked: Set["TopicPartition"]) -> None: - if self.listener: - call_result = self.listener.on_partitions_revoked(revoked) - if asyncio.iscoroutine(call_result): - await call_result - - async def on_partitions_assigned(self, assigned: Set["TopicPartition"]) -> None: - self._log( + async def on_partitions_assigned(self, assigned: set["TopicPartition"]) -> None: + self.logger.log( logging.INFO, f"Consumer {self.consumer._coordinator.member_id} assigned to partitions: " f"{assigned}", + extra=self.log_extra, ) + if not assigned: - self._log( + self.logger.log( logging.WARNING, f"Consumer in group {self.consumer._group_id} has no partition assignments - this " f"could be temporary, e.g. during a rolling update. A separate warning will be logged if " f"this condition persists for {self._log_unassigned_consumer_delay_seconds} seconds.", + extra=self.log_extra, ) + self._log_unassigned_consumer_task = asyncio.create_task( self.log_unassigned_consumer() ) + elif self._log_unassigned_consumer_task: self._log_unassigned_consumer_task.cancel() self._log_unassigned_consumer_task = None - if self.listener: - call_result = self.listener.on_partitions_assigned(assigned) - if asyncio.iscoroutine(call_result): - await call_result - def _log( +class _LoggingListenerFacade(ConsumerRebalanceListener): + def __init__( self, - log_level: int, - message: str, - extra: Optional["AnyDict"] = None, - exc_info: Optional[Exception] = None, + *, + logging_listener: _LoggingListener, + listener: ConsumerRebalanceListener, ) -> None: - if self.logger is not None: - self.logger.log( - log_level, - message, - extra=extra, - exc_info=exc_info, - ) + self.logging_listener = logging_listener + self.listener = listener + + async def on_partitions_revoked(self, revoked: set["TopicPartition"]) -> None: + await call_or_await(self.listener.on_partitions_revoked, revoked) + + async def on_partitions_assigned(self, assigned: set["TopicPartition"]) -> None: + await self.logging_listener.on_partitions_revoked(assigned) + await call_or_await(self.listener.on_partitions_assigned, assigned) diff --git a/faststream/kafka/message.py b/faststream/kafka/message.py index d5ee8c1ca7..faf34d608a 100644 --- a/faststream/kafka/message.py +++ b/faststream/kafka/message.py @@ -1,10 +1,12 @@ -from dataclasses import dataclass -from typing import Any, Protocol, Tuple, Union +from typing import Any, Protocol, Union -from aiokafka import AIOKafkaConsumer, ConsumerRecord -from aiokafka import TopicPartition as AIOKafkaTopicPartition +from aiokafka import ( + AIOKafkaConsumer, + ConsumerRecord, + TopicPartition as AIOKafkaTopicPartition, +) -from faststream.broker.message import StreamMessage +from faststream.message import AckStatus, StreamMessage class ConsumerProtocol(Protocol): @@ -37,7 +39,6 @@ def seek( FAKE_CONSUMER = FakeConsumer() -@dataclass class KafkaRawMessage(ConsumerRecord): # type: ignore[misc] consumer: AIOKafkaConsumer @@ -46,15 +47,30 @@ class KafkaMessage( StreamMessage[ Union[ "ConsumerRecord", - Tuple["ConsumerRecord", ...], + tuple["ConsumerRecord", ...], ] - ] + ], ): """Represents a Kafka message in the FastStream framework. This class extends `StreamMessage` and is specialized for handling Kafka ConsumerRecord objects. """ + def __init__(self, *args: Any, consumer: ConsumerProtocol, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.consumer = consumer + self.committed = AckStatus.ACKED + + +class KafkaAckableMessage( + StreamMessage[ + Union[ + "ConsumerRecord", + tuple["ConsumerRecord", ...], + ] + ] +): def __init__( self, *args: Any, @@ -65,6 +81,12 @@ def __init__( self.consumer = consumer + async def ack(self) -> None: + """Acknowledge the Kafka message.""" + if not self.committed: + await self.consumer.commit() + await super().ack() + async def nack(self) -> None: """Reject the Kafka message.""" if not self.committed: @@ -82,11 +104,3 @@ async def nack(self) -> None: offset=raw_message.offset, ) await super().nack() - - -class KafkaAckableMessage(KafkaMessage): - async def ack(self) -> None: - """Acknowledge the Kafka message.""" - if not self.committed: - await self.consumer.commit() - await super().ack() diff --git a/faststream/kafka/opentelemetry/middleware.py b/faststream/kafka/opentelemetry/middleware.py index 2f06486c33..41dd79870b 100644 --- a/faststream/kafka/opentelemetry/middleware.py +++ b/faststream/kafka/opentelemetry/middleware.py @@ -1,4 +1,3 @@ -from typing import Optional from opentelemetry.metrics import Meter, MeterProvider from opentelemetry.trace import TracerProvider @@ -6,16 +5,17 @@ from faststream.kafka.opentelemetry.provider import ( telemetry_attributes_provider_factory, ) +from faststream.kafka.response import KafkaPublishCommand from faststream.opentelemetry.middleware import TelemetryMiddleware -class KafkaTelemetryMiddleware(TelemetryMiddleware): +class KafkaTelemetryMiddleware(TelemetryMiddleware[KafkaPublishCommand]): def __init__( self, *, - tracer_provider: Optional[TracerProvider] = None, - meter_provider: Optional[MeterProvider] = None, - meter: Optional[Meter] = None, + tracer_provider: TracerProvider | None = None, + meter_provider: MeterProvider | None = None, + meter: Meter | None = None, ) -> None: super().__init__( settings_provider_factory=telemetry_attributes_provider_factory, diff --git a/faststream/kafka/opentelemetry/provider.py b/faststream/kafka/opentelemetry/provider.py index 0237390a2a..fe83509ce3 100644 --- a/faststream/kafka/opentelemetry/provider.py +++ b/faststream/kafka/opentelemetry/provider.py @@ -1,16 +1,19 @@ -from typing import TYPE_CHECKING, Sequence, Tuple, Union, cast +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union, cast from opentelemetry.semconv.trace import SpanAttributes -from faststream.broker.types import MsgType +from faststream._internal.types import MsgType from faststream.opentelemetry import TelemetrySettingsProvider from faststream.opentelemetry.consts import MESSAGING_DESTINATION_PUBLISH_NAME if TYPE_CHECKING: from aiokafka import ConsumerRecord - from faststream.broker.message import StreamMessage - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict + from faststream.kafka.response import KafkaPublishCommand + from faststream.message import StreamMessage + from faststream.response import PublishCommand class BaseKafkaTelemetrySettingsProvider(TelemetrySettingsProvider[MsgType]): @@ -19,33 +22,33 @@ class BaseKafkaTelemetrySettingsProvider(TelemetrySettingsProvider[MsgType]): def __init__(self) -> None: self.messaging_system = "kafka" - def get_publish_attrs_from_kwargs( + def get_publish_attrs_from_cmd( self, - kwargs: "AnyDict", + cmd: "KafkaPublishCommand", ) -> "AnyDict": - attrs = { + attrs: AnyDict = { SpanAttributes.MESSAGING_SYSTEM: self.messaging_system, - SpanAttributes.MESSAGING_DESTINATION_NAME: kwargs["topic"], - SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: kwargs["correlation_id"], + SpanAttributes.MESSAGING_DESTINATION_NAME: cmd.destination, + SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: cmd.correlation_id, } - if (partition := kwargs.get("partition")) is not None: - attrs[SpanAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION] = partition + if cmd.partition is not None: + attrs[SpanAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION] = cmd.partition - if (key := kwargs.get("key")) is not None: - attrs[SpanAttributes.MESSAGING_KAFKA_MESSAGE_KEY] = key + if cmd.key is not None: + attrs[SpanAttributes.MESSAGING_KAFKA_MESSAGE_KEY] = cmd.key return attrs def get_publish_destination_name( self, - kwargs: "AnyDict", + cmd: "PublishCommand", ) -> str: - return cast("str", kwargs["topic"]) + return cmd.destination class KafkaTelemetrySettingsProvider( - BaseKafkaTelemetrySettingsProvider["ConsumerRecord"] + BaseKafkaTelemetrySettingsProvider["ConsumerRecord"], ): def get_consume_attrs_from_message( self, @@ -74,42 +77,36 @@ def get_consume_destination_name( class BatchKafkaTelemetrySettingsProvider( - BaseKafkaTelemetrySettingsProvider[Tuple["ConsumerRecord", ...]] + BaseKafkaTelemetrySettingsProvider[tuple["ConsumerRecord", ...]], ): def get_consume_attrs_from_message( self, - msg: "StreamMessage[Tuple[ConsumerRecord, ...]]", + msg: "StreamMessage[tuple[ConsumerRecord, ...]]", ) -> "AnyDict": raw_message = msg.raw_message[0] - attrs = { + return { SpanAttributes.MESSAGING_SYSTEM: self.messaging_system, SpanAttributes.MESSAGING_MESSAGE_ID: msg.message_id, SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: msg.correlation_id, SpanAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES: len( - bytearray().join(cast("Sequence[bytes]", msg.body)) + bytearray().join(cast("Sequence[bytes]", msg.body)), ), SpanAttributes.MESSAGING_BATCH_MESSAGE_COUNT: len(msg.raw_message), SpanAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION: raw_message.partition, MESSAGING_DESTINATION_PUBLISH_NAME: raw_message.topic, } - return attrs - def get_consume_destination_name( self, - msg: "StreamMessage[Tuple[ConsumerRecord, ...]]", + msg: "StreamMessage[tuple[ConsumerRecord, ...]]", ) -> str: return cast("str", msg.raw_message[0].topic) def telemetry_attributes_provider_factory( msg: Union["ConsumerRecord", Sequence["ConsumerRecord"], None], -) -> Union[ - KafkaTelemetrySettingsProvider, - BatchKafkaTelemetrySettingsProvider, -]: +) -> KafkaTelemetrySettingsProvider | BatchKafkaTelemetrySettingsProvider: if isinstance(msg, Sequence): return BatchKafkaTelemetrySettingsProvider() - else: - return KafkaTelemetrySettingsProvider() + return KafkaTelemetrySettingsProvider() diff --git a/faststream/kafka/parser.py b/faststream/kafka/parser.py index 66e457b6c1..af543c8083 100644 --- a/faststream/kafka/parser.py +++ b/faststream/kafka/parser.py @@ -1,17 +1,20 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union, cast +from typing import TYPE_CHECKING, Any, Optional, Union, cast -from faststream.broker.message import decode_message, gen_cor_id -from faststream.kafka.message import FAKE_CONSUMER, KafkaMessage, KafkaRawMessage -from faststream.utils.context.repository import context +from faststream.kafka.message import ( + FAKE_CONSUMER, + ConsumerProtocol, + KafkaMessage, + KafkaRawMessage, +) +from faststream.message import decode_message if TYPE_CHECKING: from re import Pattern from aiokafka import ConsumerRecord - from faststream.broker.message import StreamMessage - from faststream.kafka.subscriber.usecase import LogicSubscriber - from faststream.types import DecodedMessage + from faststream._internal.basic_types import DecodedMessage + from faststream.message import StreamMessage class AioKafkaParser: @@ -19,19 +22,23 @@ class AioKafkaParser: def __init__( self, - msg_class: Type[KafkaMessage], + msg_class: type[KafkaMessage], regex: Optional["Pattern[str]"], ) -> None: self.msg_class = msg_class self.regex = regex + self._consumer: ConsumerProtocol = FAKE_CONSUMER + + def _setup(self, consumer: ConsumerProtocol) -> None: + self._consumer = consumer + async def parse_message( self, message: Union["ConsumerRecord", "KafkaRawMessage"], ) -> "StreamMessage[ConsumerRecord]": """Parses a Kafka message.""" headers = {i: j.decode() for i, j in message.headers} - handler: Optional[LogicSubscriber[Any]] = context.get_local("handler_") return self.msg_class( body=message.value or b"", @@ -39,12 +46,10 @@ async def parse_message( reply_to=headers.get("reply_to", ""), content_type=headers.get("content-type"), message_id=f"{message.offset}-{message.timestamp}", - correlation_id=headers.get("correlation_id", gen_cor_id()), + correlation_id=headers.get("correlation_id"), raw_message=message, path=self.get_path(message.topic), - consumer=getattr(message, "consumer", None) - or getattr(handler, "consumer", None) - or FAKE_CONSUMER, + consumer=getattr(message, "consumer", self._consumer), ) async def decode_message( @@ -54,21 +59,20 @@ async def decode_message( """Decodes a message.""" return decode_message(msg) - def get_path(self, topic: str) -> Dict[str, str]: + def get_path(self, topic: str) -> dict[str, str]: if self.regex and (match := self.regex.match(topic)): return match.groupdict() - else: - return {} + return {} class AioKafkaBatchParser(AioKafkaParser): async def parse_message( self, - message: Tuple["ConsumerRecord", ...], - ) -> "StreamMessage[Tuple[ConsumerRecord, ...]]": + message: tuple["ConsumerRecord", ...], + ) -> "StreamMessage[tuple[ConsumerRecord, ...]]": """Parses a batch of messages from a Kafka consumer.""" - body: List[Any] = [] - batch_headers: List[Dict[str, str]] = [] + body: list[Any] = [] + batch_headers: list[dict[str, str]] = [] first = message[0] last = message[-1] @@ -79,8 +83,6 @@ async def parse_message( headers = next(iter(batch_headers), {}) - handler: Optional[LogicSubscriber[Any]] = context.get_local("handler_") - return self.msg_class( body=body, headers=headers, @@ -88,15 +90,15 @@ async def parse_message( reply_to=headers.get("reply_to", ""), content_type=headers.get("content-type"), message_id=f"{first.offset}-{last.offset}-{first.timestamp}", - correlation_id=headers.get("correlation_id", gen_cor_id()), + correlation_id=headers.get("correlation_id"), raw_message=message, path=self.get_path(first.topic), - consumer=getattr(handler, "consumer", None) or FAKE_CONSUMER, + consumer=self._consumer, ) async def decode_message( self, - msg: "StreamMessage[Tuple[ConsumerRecord, ...]]", + msg: "StreamMessage[tuple[ConsumerRecord, ...]]", ) -> "DecodedMessage": """Decode a batch of messages.""" # super() should be here due python can't find it in comprehension diff --git a/faststream/kafka/prometheus/middleware.py b/faststream/kafka/prometheus/middleware.py index 3fd41edeba..10470cd795 100644 --- a/faststream/kafka/prometheus/middleware.py +++ b/faststream/kafka/prometheus/middleware.py @@ -1,24 +1,33 @@ -from typing import TYPE_CHECKING, Optional, Sequence +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union +from aiokafka import ConsumerRecord + +from faststream._internal.constants import EMPTY from faststream.kafka.prometheus.provider import settings_provider_factory -from faststream.prometheus.middleware import BasePrometheusMiddleware -from faststream.types import EMPTY +from faststream.kafka.response import KafkaPublishCommand +from faststream.prometheus.middleware import PrometheusMiddleware if TYPE_CHECKING: from prometheus_client import CollectorRegistry -class KafkaPrometheusMiddleware(BasePrometheusMiddleware): +class KafkaPrometheusMiddleware( + PrometheusMiddleware[ + KafkaPublishCommand, + Union[ConsumerRecord, Sequence[ConsumerRecord]], + ], +): def __init__( self, *, registry: "CollectorRegistry", app_name: str = EMPTY, metrics_prefix: str = "faststream", - received_messages_size_buckets: Optional[Sequence[float]] = None, + received_messages_size_buckets: Sequence[float] | None = None, ) -> None: super().__init__( - settings_provider_factory=settings_provider_factory, + settings_provider_factory=settings_provider_factory, # type: ignore[arg-type] registry=registry, app_name=app_name, metrics_prefix=metrics_prefix, diff --git a/faststream/kafka/prometheus/provider.py b/faststream/kafka/prometheus/provider.py index 8320f90cc5..5b5c2196b6 100644 --- a/faststream/kafka/prometheus/provider.py +++ b/faststream/kafka/prometheus/provider.py @@ -1,15 +1,14 @@ -from typing import TYPE_CHECKING, Sequence, Tuple, Union, cast +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union, cast -from faststream.broker.message import MsgType, StreamMessage -from faststream.prometheus import ( - MetricsSettingsProvider, -) +from faststream.message.message import MsgType, StreamMessage +from faststream.prometheus import MetricsSettingsProvider if TYPE_CHECKING: from aiokafka import ConsumerRecord from faststream.prometheus import ConsumeAttrs - from faststream.types import AnyDict + from faststream.response import PublishCommand class BaseKafkaMetricsSettingsProvider(MetricsSettingsProvider[MsgType]): @@ -18,11 +17,11 @@ class BaseKafkaMetricsSettingsProvider(MetricsSettingsProvider[MsgType]): def __init__(self) -> None: self.messaging_system = "kafka" - def get_publish_destination_name_from_kwargs( + def get_publish_destination_name_from_cmd( self, - kwargs: "AnyDict", + cmd: "PublishCommand", ) -> str: - return cast("str", kwargs["topic"]) + return cmd.destination class KafkaMetricsSettingsProvider(BaseKafkaMetricsSettingsProvider["ConsumerRecord"]): @@ -38,11 +37,11 @@ def get_consume_attrs_from_message( class BatchKafkaMetricsSettingsProvider( - BaseKafkaMetricsSettingsProvider[Tuple["ConsumerRecord", ...]] + BaseKafkaMetricsSettingsProvider[tuple["ConsumerRecord", ...]] ): def get_consume_attrs_from_message( self, - msg: "StreamMessage[Tuple[ConsumerRecord, ...]]", + msg: "StreamMessage[tuple[ConsumerRecord, ...]]", ) -> "ConsumeAttrs": raw_message = msg.raw_message[0] return { @@ -54,11 +53,7 @@ def get_consume_attrs_from_message( def settings_provider_factory( msg: Union["ConsumerRecord", Sequence["ConsumerRecord"], None], -) -> Union[ - KafkaMetricsSettingsProvider, - BatchKafkaMetricsSettingsProvider, -]: +) -> KafkaMetricsSettingsProvider | BatchKafkaMetricsSettingsProvider: if isinstance(msg, Sequence): return BatchKafkaMetricsSettingsProvider() - else: - return KafkaMetricsSettingsProvider() + return KafkaMetricsSettingsProvider() diff --git a/faststream/kafka/publisher/asyncapi.py b/faststream/kafka/publisher/asyncapi.py deleted file mode 100644 index 7830c01807..0000000000 --- a/faststream/kafka/publisher/asyncapi.py +++ /dev/null @@ -1,196 +0,0 @@ -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Literal, - Optional, - Sequence, - Tuple, - Union, - overload, -) - -from typing_extensions import override - -from faststream.asyncapi.schema import ( - Channel, - ChannelBinding, - CorrelationId, - Message, - Operation, -) -from faststream.asyncapi.schema.bindings import kafka -from faststream.asyncapi.utils import resolve_payloads -from faststream.broker.types import MsgType -from faststream.exceptions import SetupError -from faststream.kafka.publisher.usecase import ( - BatchPublisher, - DefaultPublisher, - LogicPublisher, -) - -if TYPE_CHECKING: - from aiokafka import ConsumerRecord - - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware - - -class AsyncAPIPublisher(LogicPublisher[MsgType]): - """A class representing a publisher.""" - - def get_name(self) -> str: - return f"{self.topic}:Publisher" - - def get_schema(self) -> Dict[str, Channel]: - payloads = self.get_payloads() - - return { - self.name: Channel( - description=self.description, - publish=Operation( - message=Message( - title=f"{self.name}:Message", - payload=resolve_payloads(payloads, "Publisher"), - correlationId=CorrelationId( - location="$message.header#/correlation_id" - ), - ), - ), - bindings=ChannelBinding(kafka=kafka.ChannelBinding(topic=self.topic)), - ) - } - - @overload # type: ignore[override] - @staticmethod - def create( - *, - batch: Literal[True], - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[Tuple[ConsumerRecord, ...]]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> "AsyncAPIBatchPublisher": ... - - @overload - @staticmethod - def create( - *, - batch: Literal[False], - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[ConsumerRecord]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> "AsyncAPIDefaultPublisher": ... - - @overload - @staticmethod - def create( - *, - batch: bool, - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence[ - "BrokerMiddleware[Union[Tuple[ConsumerRecord, ...], ConsumerRecord]]" - ], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", - ]: ... - - @override - @staticmethod - def create( - *, - batch: bool, - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence[ - "BrokerMiddleware[Union[Tuple[ConsumerRecord, ...], ConsumerRecord]]" - ], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", - ]: - if batch: - if key: - raise SetupError("You can't setup `key` with batch publisher") - - return AsyncAPIBatchPublisher( - topic=topic, - partition=partition, - headers=headers, - reply_to=reply_to, - broker_middlewares=broker_middlewares, - middlewares=middlewares, - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - else: - return AsyncAPIDefaultPublisher( - key=key, - # basic args - topic=topic, - partition=partition, - headers=headers, - reply_to=reply_to, - broker_middlewares=broker_middlewares, - middlewares=middlewares, - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - -class AsyncAPIBatchPublisher( - BatchPublisher, - AsyncAPIPublisher[Tuple["ConsumerRecord", ...]], -): - pass - - -class AsyncAPIDefaultPublisher( - DefaultPublisher, - AsyncAPIPublisher["ConsumerRecord"], -): - pass diff --git a/faststream/kafka/publisher/config.py b/faststream/kafka/publisher/config.py new file mode 100644 index 0000000000..65130aaa5b --- /dev/null +++ b/faststream/kafka/publisher/config.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass, field + +from faststream._internal.configs import ( + PublisherSpecificationConfig, + PublisherUsecaseConfig, +) +from faststream.kafka.configs import KafkaBrokerConfig + + +@dataclass(kw_only=True) +class KafkaPublisherSpecificationConfig(PublisherSpecificationConfig): + topic: str + + +@dataclass(kw_only=True) +class KafkaPublisherConfig(PublisherUsecaseConfig): + _outer_config: "KafkaBrokerConfig" = field(default_factory=KafkaBrokerConfig) + + key: bytes | str | None + topic: str + partition: int | None + headers: dict[str, str] | None + reply_to: str | None diff --git a/faststream/kafka/publisher/factory.py b/faststream/kafka/publisher/factory.py new file mode 100644 index 0000000000..083a3c1bf4 --- /dev/null +++ b/faststream/kafka/publisher/factory.py @@ -0,0 +1,83 @@ +from collections.abc import Awaitable, Callable, Sequence +from functools import wraps +from typing import ( + TYPE_CHECKING, + Any, +) + +from faststream.exceptions import SetupError + +from .config import KafkaPublisherConfig, KafkaPublisherSpecificationConfig +from .specification import KafkaPublisherSpecification +from .usecase import BatchPublisher, DefaultPublisher + +if TYPE_CHECKING: + from faststream._internal.types import PublisherMiddleware + from faststream.kafka.configs import KafkaBrokerConfig + + +def create_publisher( + *, + autoflush: bool, + batch: bool, + key: bytes | None, + topic: str, + partition: int | None, + headers: dict[str, str] | None, + reply_to: str, + # Publisher args + config: "KafkaBrokerConfig", + middlewares: Sequence["PublisherMiddleware"], + # Specification args + schema_: Any | None, + title_: str | None, + description_: str | None, + include_in_schema: bool, +): + publisher_config = KafkaPublisherConfig( + key=key, + topic=topic, + partition=partition, + headers=headers, + reply_to=reply_to, + middlewares=middlewares, + _outer_config=config, + ) + + specification = KafkaPublisherSpecification( + _outer_config=config, + specification_config=KafkaPublisherSpecificationConfig( + topic=topic, + schema_=schema_, + title_=title_, + description_=description_, + include_in_schema=include_in_schema, + ) + ) + + if batch: + if key: + msg = "You can't setup `key` with batch publisher" + raise SetupError(msg) + + publisher = BatchPublisher(publisher_config, specification) + publish_method = "_basic_publish_batch" + + else: + publisher = DefaultPublisher(publisher_config, specification) + publish_method = "_basic_publish" + + if autoflush: + default_publish: Callable[..., Awaitable[Any | None]] = getattr( + publisher, publish_method + ) + + @wraps(default_publish) + async def autoflush_wrapper(*args: Any, **kwargs: Any) -> Any | None: + result = await default_publish(*args, **kwargs) + await publisher.flush() + return result + + setattr(publisher, publish_method, autoflush_wrapper) + + return publisher diff --git a/faststream/kafka/publisher/fake.py b/faststream/kafka/publisher/fake.py new file mode 100644 index 0000000000..a3a3d8c6d8 --- /dev/null +++ b/faststream/kafka/publisher/fake.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING, Union + +from faststream._internal.endpoint.publisher.fake import FakePublisher +from faststream.kafka.response import KafkaPublishCommand + +if TYPE_CHECKING: + from faststream._internal.producer import ProducerProto + from faststream.response.response import PublishCommand + + +class KafkaFakePublisher(FakePublisher): + """Publisher Interface implementation to use as RPC or REPLY TO answer publisher.""" + + def __init__( + self, + producer: "ProducerProto", + topic: str, + ) -> None: + super().__init__(producer=producer) + self.topic = topic + + def patch_command( + self, cmd: Union["PublishCommand", "KafkaPublishCommand"] + ) -> "KafkaPublishCommand": + cmd = super().patch_command(cmd) + real_cmd = KafkaPublishCommand.from_cmd(cmd) + real_cmd.destination = self.topic + return real_cmd diff --git a/faststream/kafka/publisher/producer.py b/faststream/kafka/publisher/producer.py index fe9bfc6c50..bf7d67c585 100644 --- a/faststream/kafka/publisher/producer.py +++ b/faststream/kafka/publisher/producer.py @@ -1,111 +1,130 @@ -from typing import TYPE_CHECKING, Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union from typing_extensions import override -from faststream.broker.message import encode_message -from faststream.broker.publisher.proto import ProducerProto -from faststream.broker.utils import resolve_custom_func -from faststream.exceptions import OperationForbiddenError +from faststream._internal.endpoint.utils import resolve_custom_func +from faststream._internal.producer import ProducerProto +from faststream.exceptions import FeatureNotSupportedException from faststream.kafka.exceptions import BatchBufferOverflowException from faststream.kafka.message import KafkaMessage from faststream.kafka.parser import AioKafkaParser +from faststream.message import encode_message + +from .state import EmptyProducerState, ProducerState, RealProducer if TYPE_CHECKING: + import asyncio + from aiokafka import AIOKafkaProducer + from aiokafka.structs import RecordMetadata + from fast_depends.library.serializer import SerializerProto - from faststream.broker.types import CustomCallable - from faststream.types import SendableMessage + from faststream._internal.types import CustomCallable + from faststream.kafka.response import KafkaPublishCommand class AioKafkaFastProducer(ProducerProto): + async def connect( + self, producer: "AIOKafkaProducer", serializer: Optional["SerializerProto"] + ) -> None: ... + + async def disconnect(self) -> None: ... + + def __bool__(self) -> bool: + return False + + @property + def closed(self) -> bool: ... + + async def flush(self) -> None: ... + + async def publish( + self, cmd: "KafkaPublishCommand" + ) -> Union["asyncio.Future[RecordMetadata]", "RecordMetadata"]: ... + + async def publish_batch( + self, cmd: "KafkaPublishCommand" + ) -> Union["asyncio.Future[RecordMetadata]", "RecordMetadata"]: ... + + async def request(self, cmd: "KafkaPublishCommand") -> Any: + raise NotImplementedError + + +class AioKafkaFastProducerImpl(AioKafkaFastProducer): """A class to represent Kafka producer.""" def __init__( self, - producer: "AIOKafkaProducer", parser: Optional["CustomCallable"], decoder: Optional["CustomCallable"], ) -> None: - self._producer = producer + self._producer: ProducerState = EmptyProducerState() + self.serializer: SerializerProto | None = None # NOTE: register default parser to be compatible with request - default = AioKafkaParser( - msg_class=KafkaMessage, - regex=None, - ) + default = AioKafkaParser(msg_class=KafkaMessage, regex=None) self._parser = resolve_custom_func(parser, default.parse_message) self._decoder = resolve_custom_func(decoder, default.decode_message) + async def connect( + self, producer: "AIOKafkaProducer", serializer: Optional["SerializerProto"] + ) -> None: + self.serializer = serializer + await producer.start() + self._producer = RealProducer(producer) + + async def disconnect(self) -> None: + await self._producer.stop() + self._producer = EmptyProducerState() + + def __bool__(self) -> bool: + return bool(self._producer) + + @property + def closed(self) -> bool: + return self._producer.closed + async def flush(self) -> None: await self._producer.flush() @override async def publish( # type: ignore[override] self, - message: "SendableMessage", - topic: str, - *, - correlation_id: str, - key: Union[bytes, Any, None] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - reply_to: str = "", - no_confirm: bool = False, - ) -> None: + cmd: "KafkaPublishCommand", + ) -> Union["asyncio.Future[RecordMetadata]", "RecordMetadata"]: """Publish a message to a topic.""" - message, content_type = encode_message(message) + message, content_type = encode_message(cmd.body, serializer=self.serializer) headers_to_send = { "content-type": content_type or "", - "correlation_id": correlation_id, - **(headers or {}), + **cmd.headers_to_publish(), } - if reply_to: - headers_to_send["reply_to"] = headers_to_send.get( - "reply_to", - reply_to, - ) - - send_future = await self._producer.send( - topic=topic, + send_future = await self._producer.producer.send( + topic=cmd.destination, value=message, - key=key, - partition=partition, - timestamp_ms=timestamp_ms, + key=cmd.key, + partition=cmd.partition, + timestamp_ms=cmd.timestamp_ms, headers=[(i, (j or "").encode()) for i, j in headers_to_send.items()], ) - if not no_confirm: - await send_future - async def stop(self) -> None: - await self._producer.stop() + if not cmd.no_confirm: + return await send_future + return send_future + @override async def publish_batch( self, - *msgs: "SendableMessage", - correlation_id: str, - topic: str, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - reply_to: str = "", - no_confirm: bool = False, - ) -> None: + cmd: "KafkaPublishCommand", + ) -> Union["asyncio.Future[RecordMetadata]", "RecordMetadata"]: """Publish a batch of messages to a topic.""" - batch = self._producer.create_batch() - - headers_to_send = {"correlation_id": correlation_id, **(headers or {})} + batch = self._producer.producer.create_batch() - if reply_to: - headers_to_send["reply_to"] = headers_to_send.get( - "reply_to", - reply_to, - ) + headers_to_send = cmd.headers_to_publish() - for message_position, msg in enumerate(msgs): - message, content_type = encode_message(msg) + for message_position, body in enumerate(cmd.batch_bodies): + message, content_type = encode_message(body, serializer=self.serializer) if content_type: final_headers = { @@ -118,18 +137,56 @@ async def publish_batch( metadata = batch.append( key=None, value=message, - timestamp=timestamp_ms, + timestamp=cmd.timestamp_ms, headers=[(i, j.encode()) for i, j in final_headers.items()], ) if metadata is None: raise BatchBufferOverflowException(message_position=message_position) - send_future = await self._producer.send_batch(batch, topic, partition=partition) - if not no_confirm: - await send_future + send_future = await self._producer.producer.send_batch( + batch, + cmd.destination, + partition=cmd.partition, + ) + if not cmd.no_confirm: + return await send_future + return send_future @override - async def request(self, *args: Any, **kwargs: Any) -> Optional[Any]: - raise OperationForbiddenError( - "Kafka doesn't support `request` method without test client." - ) + async def request( + self, + cmd: "KafkaPublishCommand", + ) -> Any: + msg = "Kafka doesn't support `request` method without test client." + raise FeatureNotSupportedException(msg) + + +class FakeAioKafkaFastProducer(AioKafkaFastProducer): + async def connect(self, producer: "AIOKafkaProducer") -> None: + raise NotImplementedError + + async def disconnect(self) -> None: + raise NotImplementedError + + def __bool__(self) -> bool: + return False + + @property + def closed(self) -> bool: + raise NotImplementedError + + async def flush(self) -> None: + raise NotImplementedError + + async def publish( + self, cmd: "KafkaPublishCommand" + ) -> Union["asyncio.Future[RecordMetadata]", "RecordMetadata"]: + raise NotImplementedError + + async def publish_batch( + self, cmd: "KafkaPublishCommand" + ) -> Union["asyncio.Future[RecordMetadata]", "RecordMetadata"]: + raise NotImplementedError + + async def request(self, cmd: "KafkaPublishCommand") -> Any: + raise NotImplementedError diff --git a/faststream/kafka/publisher/specification.py b/faststream/kafka/publisher/specification.py new file mode 100644 index 0000000000..b4ae322d19 --- /dev/null +++ b/faststream/kafka/publisher/specification.py @@ -0,0 +1,43 @@ +from faststream._internal.endpoint.publisher import PublisherSpecification +from faststream.kafka.configs import KafkaBrokerConfig +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 .config import KafkaPublisherSpecificationConfig + + +class KafkaPublisherSpecification(PublisherSpecification[KafkaBrokerConfig, KafkaPublisherSpecificationConfig]): + @property + def topic(self) -> str: + return f"{self._outer_config.prefix}{self.config.topic}" + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{self.topic}:Publisher" + + def get_schema(self) -> dict[str, PublisherSpec]: + payloads = self.get_payloads() + + return { + self.name: PublisherSpec( + description=self.config.description_, + operation=Operation( + message=Message( + title=f"{self.name}:Message", + payload=resolve_payloads(payloads, "Publisher"), + ), + bindings=None, + ), + bindings=ChannelBinding( + kafka=kafka.ChannelBinding( + topic=self.topic, + partitions=None, + replicas=None, + ) + ), + ), + } diff --git a/faststream/kafka/publisher/state.py b/faststream/kafka/publisher/state.py new file mode 100644 index 0000000000..e4a661b7cc --- /dev/null +++ b/faststream/kafka/publisher/state.py @@ -0,0 +1,61 @@ +from abc import abstractmethod +from typing import TYPE_CHECKING, Protocol + +from faststream.exceptions import IncorrectState + +if TYPE_CHECKING: + from aiokafka import AIOKafkaProducer + + +class ProducerState(Protocol): + producer: "AIOKafkaProducer" + + @property + @abstractmethod + def closed(self) -> bool: ... + + def __bool__(self) -> bool: ... + + async def stop(self) -> None: ... + + async def flush(self) -> None: ... + + +class EmptyProducerState(ProducerState): + __slots__ = () + + closed = True + + @property + def producer(self) -> "AIOKafkaProducer": + msg = "You can't use producer here, please connect broker first." + raise IncorrectState(msg) + + def __bool__(self) -> bool: + return False + + async def stop(self) -> None: + pass + + async def flush(self) -> None: + pass + + +class RealProducer(ProducerState): + __slots__ = ("producer",) + + def __init__(self, producer: "AIOKafkaProducer") -> None: + self.producer = producer + + def __bool__(self) -> bool: + return True + + async def stop(self) -> None: + await self.producer.stop() + + @property + def closed(self) -> bool: + return self.producer._closed or False + + async def flush(self) -> None: + await self.producer.flush() diff --git a/faststream/kafka/publisher/usecase.py b/faststream/kafka/publisher/usecase.py index f31a6c2de4..d1471c5069 100644 --- a/faststream/kafka/publisher/usecase.py +++ b/faststream/kafka/publisher/usecase.py @@ -1,79 +1,48 @@ -from contextlib import AsyncExitStack -from functools import partial -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Dict, - Iterable, - Optional, - Sequence, - Tuple, - Union, - cast, -) +from collections.abc import Iterable +from typing import TYPE_CHECKING, Annotated, Any, Literal, Union, overload from aiokafka import ConsumerRecord -from typing_extensions import Annotated, Doc, override +from typing_extensions import Doc, override -from faststream.broker.message import SourceType, gen_cor_id -from faststream.broker.publisher.usecase import PublisherUsecase -from faststream.broker.types import MsgType +from faststream._internal.endpoint.publisher import PublisherUsecase +from faststream._internal.types import MsgType from faststream.exceptions import NOT_CONNECTED_YET -from faststream.utils.functions import return_input +from faststream.kafka.message import KafkaMessage +from faststream.kafka.response import KafkaPublishCommand +from faststream.message import gen_cor_id +from faststream.response.publish_type import PublishType if TYPE_CHECKING: - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware + import asyncio + + from aiokafka.structs import RecordMetadata + + from faststream._internal.basic_types import SendableMessage + from faststream._internal.types import PublisherMiddleware from faststream.kafka.message import KafkaMessage from faststream.kafka.publisher.producer import AioKafkaFastProducer - from faststream.types import AsyncFunc, SendableMessage + from faststream.response.response import PublishCommand + + from .config import KafkaPublisherConfig + from .specification import KafkaPublisherSpecification class LogicPublisher(PublisherUsecase[MsgType]): """A class to publish messages to a Kafka topic.""" - _producer: Optional["AioKafkaFastProducer"] - - def __init__( - self, - *, - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[MsgType]"], - middlewares: Sequence["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 - self.partition = partition - self.reply_to = reply_to - self.headers = headers + _producer: "AioKafkaFastProducer" - self._producer = None + def __init__(self, config: "KafkaPublisherConfig", specification: "KafkaPublisherSpecification") -> None: + super().__init__(config, specification) - def __hash__(self) -> int: - return hash(self.topic) + self._topic = config.topic + self.partition = config.partition + self.reply_to = config.reply_to + self.headers = config.headers or {} - def add_prefix(self, prefix: str) -> None: - self.topic = "".join((prefix, self.topic)) + @property + def topic(self) -> str: + return f"{self._outer_config.prefix}{self._topic}" @override async def request( @@ -88,7 +57,7 @@ async def request( ] = "", *, key: Annotated[ - Union[bytes, Any, None], + bytes | Any | None, Doc( """ A key to associate with the message. Can be used to @@ -98,90 +67,57 @@ async def request( partition (but if key is `None`, partition is chosen randomly). Must be type `bytes`, or be serializable to bytes via configured `key_serializer`. - """ + """, ), ] = None, partition: Annotated[ - Optional[int], + int | None, Doc( """ Specify a partition. If not set, the partition will be selected using the configured `partitioner`. - """ + """, ), ] = None, timestamp_ms: Annotated[ - Optional[int], + int | None, Doc( """ Epoch milliseconds (from Jan 1 1970 UTC) to use as the message timestamp. Defaults to current time. - """ + """, ), ] = None, headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc("Message headers to store metainformation."), ] = None, correlation_id: Annotated[ - Optional[str], + str | None, Doc( "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." + "**correlation_id** is a useful option to trace messages.", ), ] = None, timeout: Annotated[ float, Doc("Timeout to send RPC request."), ] = 0.5, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), ) -> "KafkaMessage": - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - topic = topic or self.topic - partition = partition or self.partition - headers = headers or self.headers - correlation_id = correlation_id or gen_cor_id() - - request: AsyncFunc = self._producer.request - - for pub_m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - request = partial(pub_m, request) - - published_msg = await request( + cmd = KafkaPublishCommand( message, - topic=topic, + topic=topic or self.topic, key=key, - partition=partition, - headers=headers, - timeout=timeout, - correlation_id=correlation_id, + partition=partition or self.partition, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), timestamp_ms=timestamp_ms, + timeout=timeout, + _publish_type=PublishType.REQUEST, ) - async with AsyncExitStack() as stack: - return_msg: Callable[[KafkaMessage], Awaitable[KafkaMessage]] = return_input - for m in self._broker_middlewares[::-1]: - mid = m(published_msg) - await stack.enter_async_context(mid) - return_msg = partial(mid.consume_scope, return_msg) - - parsed_msg = await self._producer._parser(published_msg) - parsed_msg._decoded_body = await self._producer._decoder(parsed_msg) - parsed_msg._source_type = SourceType.Response - return await return_msg(parsed_msg) - - raise AssertionError("unreachable") + msg: KafkaMessage = await self._basic_request(cmd) + return msg async def flush(self) -> None: assert self._producer, NOT_CONNECTED_YET # nosec B101 @@ -189,140 +125,122 @@ async def flush(self) -> None: class DefaultPublisher(LogicPublisher[ConsumerRecord]): - def __init__( + def __init__(self, config: "KafkaPublisherConfig", specification: "KafkaPublisherSpecification") -> None: + super().__init__(config, specification) + + self.key = config.key + + @overload + async def publish( self, + message: "SendableMessage", + topic: str = "", *, - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[ConsumerRecord]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - super().__init__( - topic=topic, - partition=partition, - reply_to=reply_to, - headers=headers, - # 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 + key: bytes | Any | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + reply_to: str = "", + no_confirm: Literal[True], + ) -> "asyncio.Future[RecordMetadata]": ... + + @overload + async def publish( + self, + message: "SendableMessage", + topic: str = "", + *, + key: bytes | Any | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + reply_to: str = "", + no_confirm: Literal[False] = False, + ) -> "RecordMetadata": ... @override async def publish( self, - message: Annotated[ - "SendableMessage", - Doc("Message body to send."), - ], - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ] = "", + message: "SendableMessage", + topic: str = "", *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - timestamp_ms: Annotated[ - Optional[int], - Doc( - """ - Epoch milliseconds (from Jan 1 1970 UTC) to use as - the message timestamp. Defaults to current time. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc("Message headers to store metainformation."), - ] = None, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Reply message topic name to send response."), - ] = "", - no_confirm: Annotated[ - bool, - Doc("Do not wait for Kafka publish confirmation."), - ] = False, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), - ) -> Optional[Any]: - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - topic = topic or self.topic - key = key or self.key - partition = partition or self.partition - headers = headers or self.headers - reply_to = reply_to or self.reply_to - correlation_id = correlation_id or gen_cor_id() - - call: AsyncFunc = self._producer.publish - - for m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - call = partial(m, call) - - return await call( + key: bytes | Any | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + reply_to: str = "", + no_confirm: bool = False, + ) -> Union["asyncio.Future[RecordMetadata]", "RecordMetadata"]: + """Publishes a message to Kafka. + + Args: + message: + Message body to send. + topic: + Topic where the message will be published. + key: + A key to associate with the message. Can be used to + determine which partition to send the message to. If partition + is `None` (and producer's partitioner config is left as default), + then messages with the same key will be delivered to the same + partition (but if key is `None`, partition is chosen randomly). + Must be type `bytes`, or be serializable to bytes via configured + `key_serializer` + partition: + Specify a partition. If not set, the partition will be + selected using the configured `partitioner` + timestamp_ms: + Epoch milliseconds (from Jan 1 1970 UTC) to use as + the message timestamp. Defaults to current time. + headers: + Message headers to store metainformation. + correlation_id: + Manual message **correlation_id** setter. + **correlation_id** is a useful option to trace messages. + reply_to: + Reply message topic name to send response. + no_confirm: + Do not wait for Kafka publish confirmation. + + Returns: + `asyncio.Future[RecordMetadata]` if no_confirm = True. + `RecordMetadata` if no_confirm = False. + """ + cmd = KafkaPublishCommand( message, - topic=topic, - key=key, - partition=partition, - headers=headers, - reply_to=reply_to, - correlation_id=correlation_id, + topic=topic or self.topic, + key=key or self.key, + partition=partition or self.partition, + reply_to=reply_to or self.reply_to, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), timestamp_ms=timestamp_ms, no_confirm=no_confirm, + _publish_type=PublishType.PUBLISH, ) + return await self._basic_publish(cmd, _extra_middlewares=()) + + @override + async def _publish( + self, + cmd: Union["PublishCommand", "KafkaPublishCommand"], + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> None: + """This method should be called in subscriber flow only.""" + cmd = KafkaPublishCommand.from_cmd(cmd) + + cmd.destination = self.topic + cmd.add_headers(self.headers, override=False) + cmd.reply_to = cmd.reply_to or self.reply_to + + cmd.partition = cmd.partition or self.partition + cmd.key = cmd.key or self.key + + await self._basic_publish(cmd, _extra_middlewares=_extra_middlewares) @override async def request( @@ -337,7 +255,7 @@ async def request( ] = "", *, key: Annotated[ - Union[bytes, Any, None], + bytes | Any | None, Doc( """ A key to associate with the message. Can be used to @@ -347,50 +265,45 @@ async def request( partition (but if key is `None`, partition is chosen randomly). Must be type `bytes`, or be serializable to bytes via configured `key_serializer`. - """ + """, ), ] = None, partition: Annotated[ - Optional[int], + int | None, Doc( """ Specify a partition. If not set, the partition will be selected using the configured `partitioner`. - """ + """, ), ] = None, timestamp_ms: Annotated[ - Optional[int], + int | None, Doc( """ Epoch milliseconds (from Jan 1 1970 UTC) to use as the message timestamp. Defaults to current time. - """ + """, ), ] = None, headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc("Message headers to store metainformation."), ] = None, correlation_id: Annotated[ - Optional[str], + str | None, Doc( "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." + "**correlation_id** is a useful option to trace messages.", ), ] = None, timeout: Annotated[ float, Doc("Timeout to send RPC request."), ] = 0.5, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), ) -> "KafkaMessage": return await super().request( - message=message, + message, topic=topic, key=key or self.key, partition=partition, @@ -398,101 +311,104 @@ async def request( headers=headers, correlation_id=correlation_id, timeout=timeout, - _extra_middlewares=_extra_middlewares, ) -class BatchPublisher(LogicPublisher[Tuple["ConsumerRecord", ...]]): - @override +class BatchPublisher(LogicPublisher[tuple["ConsumerRecord", ...]]): + @overload async def publish( self, - message: Annotated[ - Union["SendableMessage", Iterable["SendableMessage"]], - Doc("One message or iterable messages bodies to send."), - ], - *extra_messages: Annotated[ - "SendableMessage", - Doc("Messages bodies to send."), - ], - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ] = "", - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - timestamp_ms: Annotated[ - Optional[int], - Doc( - """ - Epoch milliseconds (from Jan 1 1970 UTC) to use as - the message timestamp. Defaults to current time. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc("Messages headers to store metainformation."), - ] = None, - reply_to: Annotated[ - str, - Doc("Reply message topic name to send response."), - ] = "", - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - no_confirm: Annotated[ - bool, - Doc("Do not wait for Kafka publish confirmation."), - ] = False, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), - ) -> None: - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - msgs: Iterable[SendableMessage] - if extra_messages: - msgs = (cast("SendableMessage", message), *extra_messages) - else: - msgs = cast("Iterable[SendableMessage]", message) - - topic = topic or self.topic - partition = partition or self.partition - headers = headers or self.headers - reply_to = reply_to or self.reply_to - correlation_id = correlation_id or gen_cor_id() - - call: AsyncFunc = self._producer.publish_batch - - for m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - call = partial(m, call) + *messages: "SendableMessage", + topic: str = "", + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + correlation_id: str | None = None, + no_confirm: Literal[True], + ) -> "asyncio.Future[RecordMetadata]": ... + + @overload + async def publish( + self, + *messages: "SendableMessage", + topic: str = "", + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + correlation_id: str | None = None, + no_confirm: Literal[False] = False, + ) -> "RecordMetadata": ... - await call( - *msgs, - topic=topic, - partition=partition, - headers=headers, - reply_to=reply_to, - correlation_id=correlation_id, + @override + async def publish( + self, + *messages: "SendableMessage", + topic: str = "", + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + correlation_id: str | None = None, + no_confirm: bool = False, + ) -> Union["asyncio.Future[RecordMetadata]", "RecordMetadata"]: + """Publish a message batch as a single request to broker. + + Args: + *messages: + Messages bodies to send. + topic: + Topic where the message will be published. + partition: + Specify a partition. If not set, the partition will be + selected using the configured `partitioner` + timestamp_ms: + Epoch milliseconds (from Jan 1 1970 UTC) to use as + the message timestamp. Defaults to current time. + headers: + Message headers to store metainformation. + reply_to: + Reply message topic name to send response. + correlation_id: + Manual message **correlation_id** setter. + **correlation_id** is a useful option to trace messages. + no_confirm: + Do not wait for Kafka publish confirmation. + + Returns: + `asyncio.Future[RecordMetadata]` if no_confirm = True. + `RecordMetadata` if no_confirm = False. + """ + cmd = KafkaPublishCommand( + *messages, + key=None, + topic=topic or self.topic, + partition=partition or self.partition, + reply_to=reply_to or self.reply_to, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), timestamp_ms=timestamp_ms, no_confirm=no_confirm, + _publish_type=PublishType.PUBLISH, ) + + return await self._basic_publish_batch(cmd, _extra_middlewares=()) + + @override + async def _publish( + self, + cmd: Union["PublishCommand", "KafkaPublishCommand"], + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> None: + """This method should be called in subscriber flow only.""" + cmd = KafkaPublishCommand.from_cmd(cmd, batch=True) + + cmd.destination = self.topic + cmd.add_headers(self.headers, override=False) + cmd.reply_to = cmd.reply_to or self.reply_to + + cmd.partition = cmd.partition or self.partition + + await self._basic_publish_batch(cmd, _extra_middlewares=_extra_middlewares) diff --git a/faststream/kafka/response.py b/faststream/kafka/response.py index da420aa286..31aba9ea4c 100644 --- a/faststream/kafka/response.py +++ b/faststream/kafka/response.py @@ -1,11 +1,12 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, Optional, Union from typing_extensions import override -from faststream.broker.response import Response +from faststream.response.publish_type import PublishType +from faststream.response.response import BatchPublishCommand, PublishCommand, Response if TYPE_CHECKING: - from faststream.types import AnyDict, SendableMessage + from faststream._internal.basic_types import AnyDict, SendableMessage class KafkaResponse(Response): @@ -14,9 +15,9 @@ def __init__( body: "SendableMessage", *, headers: Optional["AnyDict"] = None, - correlation_id: Optional[str] = None, - timestamp_ms: Optional[int] = None, - key: Optional[bytes] = None, + correlation_id: str | None = None, + timestamp_ms: int | None = None, + key: bytes | None = None, ) -> None: super().__init__( body=body, @@ -28,10 +29,84 @@ def __init__( self.key = key @override - def as_publish_kwargs(self) -> "AnyDict": - publish_options = { - **super().as_publish_kwargs(), - "timestamp_ms": self.timestamp_ms, - "key": self.key, - } - return publish_options + def as_publish_command(self) -> "KafkaPublishCommand": + return KafkaPublishCommand( + self.body, + headers=self.headers, + correlation_id=self.correlation_id, + _publish_type=PublishType.PUBLISH, + # Kafka specific + topic="", + key=self.key, + timestamp_ms=self.timestamp_ms, + ) + + +class KafkaPublishCommand(BatchPublishCommand): + def __init__( + self, + message: "SendableMessage", + /, + *messages: "SendableMessage", + topic: str, + _publish_type: PublishType, + key: bytes | Any | None = None, + partition: int | None = None, + timestamp_ms: int | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + reply_to: str = "", + no_confirm: bool = False, + timeout: float = 0.5, + ) -> None: + super().__init__( + message, + *messages, + destination=topic, + reply_to=reply_to, + correlation_id=correlation_id, + headers=headers, + _publish_type=_publish_type, + ) + + self.key = key + self.partition = partition + self.timestamp_ms = timestamp_ms + self.no_confirm = no_confirm + + # request option + self.timeout = timeout + + @classmethod + def from_cmd( + cls, + cmd: Union["PublishCommand", "KafkaPublishCommand"], + *, + batch: bool = False, + ) -> "KafkaPublishCommand": + if isinstance(cmd, KafkaPublishCommand): + # NOTE: Should return a copy probably. + return cmd + + body, extra_bodies = cls._parse_bodies(cmd.body, batch=batch) + + return cls( + body, + *extra_bodies, + topic=cmd.destination, + correlation_id=cmd.correlation_id, + headers=cmd.headers, + reply_to=cmd.reply_to, + _publish_type=cmd.publish_type, + ) + + def headers_to_publish(self) -> dict[str, str]: + headers = {} + + if self.correlation_id: + headers["correlation_id"] = self.correlation_id + + if self.reply_to: + headers["reply_to"] = self.reply_to + + return headers | self.headers diff --git a/faststream/kafka/router.py b/faststream/kafka/router.py deleted file mode 100644 index 71638c140e..0000000000 --- a/faststream/kafka/router.py +++ /dev/null @@ -1,640 +0,0 @@ -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Dict, - Iterable, - Literal, - Optional, - Sequence, - Tuple, - Union, -) - -from aiokafka.coordinator.assignors.roundrobin import RoundRobinPartitionAssignor -from typing_extensions import Annotated, Doc, deprecated - -from faststream.broker.router import ArgsContainer, BrokerRouter, SubscriberRoute -from faststream.broker.utils import default_filter -from faststream.kafka.broker.registrator import KafkaRegistrator - -if TYPE_CHECKING: - from aiokafka import ConsumerRecord, TopicPartition - from aiokafka.abc import ConsumerRebalanceListener - from aiokafka.coordinator.assignors.abstract import AbstractPartitionAssignor - from fast_depends.dependencies import Depends - - from faststream.broker.types import ( - BrokerMiddleware, - CustomCallable, - Filter, - PublisherMiddleware, - SubscriberMiddleware, - ) - from faststream.kafka.message import KafkaMessage - from faststream.types import SendableMessage - - -class KafkaPublisher(ArgsContainer): - """Delayed KafkaPublisher registration object. - - Just a copy of `KafkaRegistrator.publisher(...)` arguments. - """ - - def __init__( - self, - topic: Annotated[ - str, - Doc("Topic where the message will be published."), - ], - *, - key: Annotated[ - Union[bytes, Any, None], - Doc( - """ - A key to associate with the message. Can be used to - determine which partition to send the message to. If partition - is `None` (and producer's partitioner config is left as default), - then messages with the same key will be delivered to the same - partition (but if key is `None`, partition is chosen randomly). - Must be type `bytes`, or be serializable to bytes via configured - `key_serializer`. - """ - ), - ] = None, - partition: Annotated[ - Optional[int], - Doc( - """ - Specify a partition. If not set, the partition will be - selected using the configured `partitioner`. - """ - ), - ] = None, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Topic name to send response."), - ] = "", - batch: Annotated[ - bool, - Doc("Whether to send messages in batches or not."), - ] = False, - # basic args - middlewares: Annotated[ - Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - ) -> None: - super().__init__( - topic=topic, - key=key, - partition=partition, - batch=batch, - headers=headers, - reply_to=reply_to, - # basic args - middlewares=middlewares, - # AsyncAPI args - title=title, - description=description, - schema=schema, - include_in_schema=include_in_schema, - ) - - -class KafkaRoute(SubscriberRoute): - """Class to store delayed KafkaBroker subscriber registration.""" - - def __init__( - self, - call: Annotated[ - Union[ - Callable[..., "SendableMessage"], - Callable[..., Awaitable["SendableMessage"]], - ], - Doc( - "Message handler function " - "to wrap the same with `@broker.subscriber(...)` way." - ), - ], - *topics: Annotated[ - str, - Doc("Kafka topics to consume messages from."), - ], - publishers: Annotated[ - Iterable[KafkaPublisher], - Doc("Kafka publishers to broadcast the handler result."), - ] = (), - batch: Annotated[ - bool, - Doc("Whether to consume messages in batches or not."), - ] = False, - group_id: Annotated[ - Optional[str], - Doc( - """ - Name of the consumer group to join for dynamic - partition assignment (if enabled), and to use for fetching and - committing offsets. If `None`, auto-partition assignment (via - group coordinator) and offset commits are disabled. - """ - ), - ] = None, - key_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], - Doc( - "Any callable that takes a raw message `bytes` " - "key and returns a deserialized one." - ), - ] = None, - value_deserializer: Annotated[ - Optional[Callable[[bytes], Any]], - Doc( - "Any callable that takes a raw message `bytes` " - "value and returns a deserialized value." - ), - ] = None, - fetch_max_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data the server should - return for a fetch request. This is not an absolute maximum, if - the first message in the first non-empty partition of the fetch - is larger than this value, the message will still be returned - to ensure that the consumer can make progress. NOTE: consumer - performs fetches to multiple brokers in parallel so memory - usage will depend on the number of brokers containing - partitions for the topic. - """ - ), - ] = 50 * 1024 * 1024, - fetch_min_bytes: Annotated[ - int, - Doc( - """ - Minimum amount of data the server should - return for a fetch request, otherwise wait up to - `fetch_max_wait_ms` for more data to accumulate. - """ - ), - ] = 1, - fetch_max_wait_ms: Annotated[ - int, - Doc( - """ - The maximum amount of time in milliseconds - the server will block before answering the fetch request if - there isn't sufficient data to immediately satisfy the - requirement given by `fetch_min_bytes`. - """ - ), - ] = 500, - max_partition_fetch_bytes: Annotated[ - int, - Doc( - """ - The maximum amount of data - per-partition the server will return. The maximum total memory - used for a request ``= #partitions * max_partition_fetch_bytes``. - This size must be at least as large as the maximum message size - the server allows or else it is possible for the producer to - send messages larger than the consumer can fetch. If that - happens, the consumer can get stuck trying to fetch a large - message on a certain partition. - """ - ), - ] = 1 * 1024 * 1024, - auto_offset_reset: Annotated[ - Literal["latest", "earliest", "none"], - Doc( - """ - A policy for resetting offsets on `OffsetOutOfRangeError` errors: - - * `earliest` will move to the oldest available message - * `latest` will move to the most recent - * `none` will raise an exception so you can handle this case - """ - ), - ] = "latest", - auto_commit: Annotated[ - bool, - Doc( - """ - If `True` the consumer's offset will be - periodically committed in the background. - """ - ), - ] = True, - auto_commit_interval_ms: Annotated[ - int, - Doc( - """ - Milliseconds between automatic - offset commits, if `auto_commit` is `True`.""" - ), - ] = 5 * 1000, - check_crcs: Annotated[ - bool, - Doc( - """ - Automatically check the CRC32 of the records - consumed. This ensures no on-the-wire or on-disk corruption to - the messages occurred. This check adds some overhead, so it may - be disabled in cases seeking extreme performance. - """ - ), - ] = True, - partition_assignment_strategy: Annotated[ - Sequence["AbstractPartitionAssignor"], - Doc( - """ - List of objects to use to - distribute partition ownership amongst consumer instances when - group management is used. This preference is implicit in the order - of the strategies in the list. When assignment strategy changes: - to support a change to the assignment strategy, new versions must - enable support both for the old assignment strategy and the new - one. The coordinator will choose the old assignment strategy until - all members have been updated. Then it will choose the new - strategy. - """ - ), - ] = (RoundRobinPartitionAssignor,), - max_poll_interval_ms: Annotated[ - int, - Doc( - """ - Maximum allowed time between calls to - consume messages in batches. If this interval - is exceeded the consumer is considered failed and the group will - rebalance in order to reassign the partitions to another consumer - group member. If API methods block waiting for messages, that time - does not count against this timeout. - """ - ), - ] = 5 * 60 * 1000, - rebalance_timeout_ms: Annotated[ - Optional[int], - Doc( - """ - The maximum time server will wait for this - consumer to rejoin the group in a case of rebalance. In Java client - this behaviour is bound to `max.poll.interval.ms` configuration, - but as ``aiokafka`` will rejoin the group in the background, we - decouple this setting to allow finer tuning by users that use - `ConsumerRebalanceListener` to delay rebalacing. Defaults - to ``session_timeout_ms`` - """ - ), - ] = None, - session_timeout_ms: Annotated[ - int, - Doc( - """ - Client group session and failure detection - timeout. The consumer sends periodic heartbeats - (`heartbeat.interval.ms`) to indicate its liveness to the broker. - If no hearts are received by the broker for a group member within - the session timeout, the broker will remove the consumer from the - group and trigger a rebalance. The allowed range is configured with - the **broker** configuration properties - `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. - """ - ), - ] = 10 * 1000, - heartbeat_interval_ms: Annotated[ - int, - Doc( - """ - The expected time in milliseconds - between heartbeats to the consumer coordinator when using - Kafka's group management feature. Heartbeats are used to ensure - that the consumer's session stays active and to facilitate - rebalancing when new consumers join or leave the group. The - value must be set lower than `session_timeout_ms`, but typically - should be set no higher than 1/3 of that value. It can be - adjusted even lower to control the expected time for normal - rebalances. - """ - ), - ] = 3 * 1000, - consumer_timeout_ms: Annotated[ - int, - Doc( - """ - Maximum wait timeout for background fetching - routine. Mostly defines how fast the system will see rebalance and - request new data for new partitions. - """ - ), - ] = 200, - max_poll_records: Annotated[ - Optional[int], - Doc( - """ - The maximum number of records returned in a - single call by batch consumer. Has no limit by default. - """ - ), - ] = None, - exclude_internal_topics: Annotated[ - bool, - Doc( - """ - Whether records from internal topics - (such as offsets) should be exposed to the consumer. If set to True - the only way to receive records from an internal topic is - subscribing to it. - """ - ), - ] = True, - isolation_level: Annotated[ - Literal["read_uncommitted", "read_committed"], - Doc( - """ - Controls how to read messages written - transactionally. - - * `read_committed`, batch consumer will only return - transactional messages which have been committed. - - * `read_uncommitted` (the default), batch consumer will - return all messages, even transactional messages which have been - aborted. - - Non-transactional messages will be returned unconditionally in - either mode. - - Messages will always be returned in offset order. Hence, in - `read_committed` mode, batch consumer will only return - messages up to the last stable offset (LSO), which is the one less - than the offset of the first open transaction. In particular any - messages appearing after messages belonging to ongoing transactions - will be withheld until the relevant transaction has been completed. - As a result, `read_committed` consumers will not be able to read up - to the high watermark when there are in flight transactions. - Further, when in `read_committed` the seek_to_end method will - return the LSO. See method docs below. - """ - ), - ] = "read_uncommitted", - batch_timeout_ms: Annotated[ - int, - Doc( - """ - Milliseconds spent waiting if - data is not available in the buffer. If 0, returns immediately - with any records that are available currently in the buffer, - else returns empty. - """ - ), - ] = 200, - max_records: Annotated[ - Optional[int], - Doc("Number of messages to consume as one batch."), - ] = None, - listener: Annotated[ - Optional["ConsumerRebalanceListener"], - Doc( - """ - Optionally include listener - callback, which will be called before and after each rebalance - operation. - As part of group management, the consumer will keep track of - the list of consumers that belong to a particular group and - will trigger a rebalance operation if one of the following - events trigger: - - * Number of partitions change for any of the subscribed topics - * Topic is created or deleted - * An existing member of the consumer group dies - * A new member is added to the consumer group - - When any of these events are triggered, the provided listener - will be invoked first to indicate that the consumer's - assignment has been revoked, and then again when the new - assignment has been received. Note that this listener will - immediately override any listener set in a previous call - to subscribe. It is guaranteed, however, that the partitions - revoked/assigned - through this interface are from topics subscribed in this call. - """ - ), - ] = None, - pattern: Annotated[ - Optional[str], - Doc( - """ - Pattern to match available topics. You must provide either topics or pattern, but not both. - """ - ), - ] = None, - partitions: Annotated[ - Optional[Iterable["TopicPartition"]], - Doc( - """ - A topic and partition tuple. You can't use 'topics' and 'partitions' in the same time. - """ - ), - ] = (), - # broker args - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **ConsumerRecord** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - middlewares: Annotated[ - Sequence["SubscriberMiddleware[KafkaMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[KafkaMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), - deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - max_workers: Annotated[ - int, - Doc("Number of workers to process messages concurrently."), - ] = 1, - ) -> None: - super().__init__( - call, - *topics, - publishers=publishers, - max_workers=max_workers, - group_id=group_id, - key_deserializer=key_deserializer, - value_deserializer=value_deserializer, - fetch_max_wait_ms=fetch_max_wait_ms, - fetch_max_bytes=fetch_max_bytes, - fetch_min_bytes=fetch_min_bytes, - max_partition_fetch_bytes=max_partition_fetch_bytes, - auto_offset_reset=auto_offset_reset, - auto_commit=auto_commit, - auto_commit_interval_ms=auto_commit_interval_ms, - check_crcs=check_crcs, - partition_assignment_strategy=partition_assignment_strategy, - max_poll_interval_ms=max_poll_interval_ms, - rebalance_timeout_ms=rebalance_timeout_ms, - session_timeout_ms=session_timeout_ms, - heartbeat_interval_ms=heartbeat_interval_ms, - consumer_timeout_ms=consumer_timeout_ms, - max_poll_records=max_poll_records, - exclude_internal_topics=exclude_internal_topics, - isolation_level=isolation_level, - max_records=max_records, - batch_timeout_ms=batch_timeout_ms, - batch=batch, - listener=listener, - pattern=pattern, - partitions=partitions, - # basic args - dependencies=dependencies, - parser=parser, - decoder=decoder, - middlewares=middlewares, - filter=filter, - no_reply=no_reply, - # AsyncAPI args - title=title, - description=description, - include_in_schema=include_in_schema, - # FastDepends args - retry=retry, - no_ack=no_ack, - ) - - -class KafkaRouter( - KafkaRegistrator, - BrokerRouter[ - Union[ - "ConsumerRecord", - Tuple["ConsumerRecord", ...], - ] - ], -): - """Includable to KafkaBroker router.""" - - def __init__( - self, - prefix: Annotated[ - str, - Doc("String prefix to add to all subscribers queues."), - ] = "", - handlers: Annotated[ - Iterable[KafkaRoute], - Doc("Route object to include."), - ] = (), - *, - dependencies: Annotated[ - Iterable["Depends"], - Doc( - "Dependencies list (`[Depends(),]`) to apply to all routers' publishers/subscribers." - ), - ] = (), - middlewares: Annotated[ - Sequence[ - Union[ - "BrokerMiddleware[ConsumerRecord]", - "BrokerMiddleware[Tuple[ConsumerRecord, ...]]", - ] - ], - Doc("Router middlewares to apply to all routers' publishers/subscribers."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **ConsumerRecord** object to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - include_in_schema: Annotated[ - Optional[bool], - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = None, - ) -> None: - super().__init__( - handlers=handlers, - # basic args - prefix=prefix, - dependencies=dependencies, - middlewares=middlewares, - parser=parser, - decoder=decoder, - include_in_schema=include_in_schema, - ) diff --git a/faststream/kafka/schemas/params.py b/faststream/kafka/schemas/params.py index e6e18b4a47..793e8801dc 100644 --- a/faststream/kafka/schemas/params.py +++ b/faststream/kafka/schemas/params.py @@ -1,6 +1,6 @@ import ssl from asyncio import AbstractEventLoop -from typing import List, Literal, Optional, Union +from typing import Literal from aiokafka.abc import AbstractTokenProvider from typing_extensions import TypedDict @@ -28,8 +28,8 @@ class AdminClientConnectionParams(TypedDict, total=False): sasl_oauth_token_provider : OAuthBearer token provider instance. """ - bootstrap_servers: Union[str, List[str]] - loop: Optional[AbstractEventLoop] + bootstrap_servers: str | list[str] + loop: AbstractEventLoop | None client_id: str request_timeout_ms: int retry_backoff_ms: int @@ -77,8 +77,8 @@ class ConsumerConnectionParams(TypedDict, total=False): sasl_oauth_token_provider : OAuthBearer token provider instance. """ - bootstrap_servers: Union[str, List[str]] - loop: Optional[AbstractEventLoop] + bootstrap_servers: str | list[str] + loop: AbstractEventLoop | None client_id: str request_timeout_ms: int retry_backoff_ms: int diff --git a/faststream/kafka/security.py b/faststream/kafka/security.py index 1f08878c1c..6ad70ab886 100644 --- a/faststream/kafka/security.py +++ b/faststream/kafka/security.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from faststream.security import ( SASLGSSAPI, @@ -10,26 +10,26 @@ ) if TYPE_CHECKING: - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict -def parse_security(security: Optional[BaseSecurity]) -> "AnyDict": +def parse_security(security: BaseSecurity | None) -> "AnyDict": if security is None: return {} - elif isinstance(security, SASLPlaintext): + if isinstance(security, SASLPlaintext): return _parse_sasl_plaintext(security) - elif isinstance(security, SASLScram256): + if isinstance(security, SASLScram256): return _parse_sasl_scram256(security) - elif isinstance(security, SASLScram512): + if isinstance(security, SASLScram512): return _parse_sasl_scram512(security) - elif isinstance(security, SASLOAuthBearer): + if isinstance(security, SASLOAuthBearer): return _parse_sasl_oauthbearer(security) - elif isinstance(security, SASLGSSAPI): + if isinstance(security, SASLGSSAPI): return _parse_sasl_gssapi(security) - elif isinstance(security, BaseSecurity): + if isinstance(security, BaseSecurity): return _parse_base_security(security) - else: - raise NotImplementedError(f"KafkaBroker does not support `{type(security)}`.") + msg = f"KafkaBroker does not support `{type(security)}`." + raise NotImplementedError(msg) def _parse_base_security(security: BaseSecurity) -> "AnyDict": diff --git a/faststream/kafka/subscriber/asyncapi.py b/faststream/kafka/subscriber/asyncapi.py deleted file mode 100644 index 4ea1e376fe..0000000000 --- a/faststream/kafka/subscriber/asyncapi.py +++ /dev/null @@ -1,90 +0,0 @@ -from itertools import chain -from typing import ( - TYPE_CHECKING, - Dict, - Tuple, -) - -from faststream.asyncapi.schema import ( - Channel, - ChannelBinding, - CorrelationId, - Message, - Operation, -) -from faststream.asyncapi.schema.bindings import kafka -from faststream.asyncapi.utils import resolve_payloads -from faststream.broker.types import MsgType -from faststream.kafka.subscriber.usecase import ( - BatchSubscriber, - ConcurrentBetweenPartitionsSubscriber, - ConcurrentDefaultSubscriber, - DefaultSubscriber, - LogicSubscriber, -) - -if TYPE_CHECKING: - from aiokafka import ConsumerRecord - - -class AsyncAPISubscriber(LogicSubscriber[MsgType]): - """A class to handle logic and async API operations.""" - - def get_name(self) -> str: - return f"{','.join(self.topics)}:{self.call_name}" - - def get_schema(self) -> Dict[str, Channel]: - channels = {} - - payloads = self.get_payloads() - - topics = chain(self.topics, {part.topic for part in self.partitions}) - - for t in topics: - handler_name = self.title_ or f"{t}:{self.call_name}" - - channels[handler_name] = Channel( - description=self.description, - subscribe=Operation( - message=Message( - title=f"{handler_name}:Message", - payload=resolve_payloads(payloads), - correlationId=CorrelationId( - location="$message.header#/correlation_id" - ), - ), - ), - bindings=ChannelBinding( - kafka=kafka.ChannelBinding(topic=t), - ), - ) - - return channels - - -class AsyncAPIDefaultSubscriber( - DefaultSubscriber, - AsyncAPISubscriber["ConsumerRecord"], -): - pass - - -class AsyncAPIBatchSubscriber( - BatchSubscriber, - AsyncAPISubscriber[Tuple["ConsumerRecord", ...]], -): - pass - - -class AsyncAPIConcurrentDefaultSubscriber( - AsyncAPISubscriber["ConsumerRecord"], - ConcurrentDefaultSubscriber, -): - pass - - -class AsyncAPIConcurrentBetweenPartitionsSubscriber( - ConcurrentBetweenPartitionsSubscriber, - AsyncAPISubscriber["ConsumerRecord"], -): - pass diff --git a/faststream/kafka/subscriber/config.py b/faststream/kafka/subscriber/config.py new file mode 100644 index 0000000000..ddd42a5f09 --- /dev/null +++ b/faststream/kafka/subscriber/config.py @@ -0,0 +1,66 @@ +from collections.abc import Iterable, Sequence +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Optional + +from faststream._internal.configs import ( + SubscriberSpecificationConfig, + SubscriberUsecaseConfig, +) +from faststream._internal.constants import EMPTY +from faststream.kafka.configs import KafkaBrokerConfig +from faststream.middlewares import AckPolicy + +if TYPE_CHECKING: + from aiokafka import TopicPartition + from aiokafka.abc import ConsumerRebalanceListener + + from faststream._internal.basic_types import AnyDict + + +@dataclass(kw_only=True) +class KafkaSubscriberSpecificationConfig(SubscriberSpecificationConfig): + topics: Sequence[str] = field(default_factory=list) + partitions: Iterable["TopicPartition"] = field(default_factory=list) + + +@dataclass(kw_only=True) +class KafkaSubscriberConfig(SubscriberUsecaseConfig): + _outer_config: "KafkaBrokerConfig" = field(default_factory=KafkaBrokerConfig) + + topics: Sequence[str] = field(default_factory=list) + group_id: str | None = None + connection_args: "AnyDict" = field(default_factory=dict) + listener: Optional["ConsumerRebalanceListener"] = None + pattern: str | None = None + partitions: Iterable["TopicPartition"] = field(default_factory=list) + + _auto_commit: bool = field(default_factory=lambda: EMPTY, repr=False) + _no_ack: bool = field(default_factory=lambda: EMPTY, repr=False) + + def __post_init__(self) -> None: + if self.ack_first: + self.connection_args["enable_auto_commit"] = True + + @property + def ack_first(self) -> bool: + return self.__ack_policy is AckPolicy.ACK_FIRST + + @property + def ack_policy(self) -> AckPolicy: + if (policy := self.__ack_policy) is AckPolicy.ACK_FIRST: + return AckPolicy.DO_NOTHING + + return policy + + @property + def __ack_policy(self) -> AckPolicy: + if self._auto_commit is not EMPTY and self._auto_commit: + return AckPolicy.ACK_FIRST + + if self._no_ack is not EMPTY and self._no_ack: + return AckPolicy.DO_NOTHING + + if self._ack_policy is EMPTY: + return AckPolicy.ACK_FIRST + + return self._ack_policy diff --git a/faststream/kafka/subscriber/factory.py b/faststream/kafka/subscriber/factory.py index 0f913572e4..ecc2efa0c2 100644 --- a/faststream/kafka/subscriber/factory.py +++ b/faststream/kafka/subscriber/factory.py @@ -1,404 +1,188 @@ -from functools import wraps -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Collection, - Dict, - Iterable, - Literal, - Optional, - Sequence, - Tuple, - Union, - overload, -) +import warnings +from collections.abc import Collection, Iterable +from typing import TYPE_CHECKING, Optional, Union +from faststream._internal.constants import EMPTY +from faststream._internal.endpoint.subscriber.call_item import CallsCollection from faststream.exceptions import SetupError -from faststream.kafka.publisher.asyncapi import ( - AsyncAPIBatchPublisher, - AsyncAPIDefaultPublisher, -) -from faststream.kafka.subscriber.asyncapi import ( - AsyncAPIBatchSubscriber, - AsyncAPIConcurrentBetweenPartitionsSubscriber, - AsyncAPIConcurrentDefaultSubscriber, - AsyncAPIDefaultSubscriber, +from faststream.middlewares import AckPolicy + +from .config import KafkaSubscriberConfig, KafkaSubscriberSpecificationConfig +from .specification import KafkaSubscriberSpecification +from .usecase import ( + BatchSubscriber, + ConcurrentBetweenPartitionsSubscriber, + ConcurrentDefaultSubscriber, + DefaultSubscriber, ) if TYPE_CHECKING: - from aiokafka import ConsumerRecord, TopicPartition + from aiokafka import TopicPartition from aiokafka.abc import ConsumerRebalanceListener - from fast_depends.dependencies import Depends - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware - from faststream.types import AnyDict - - -@overload -def create_subscriber( - *topics: str, - batch: Literal[True], - batch_timeout_ms: int, - max_records: Optional[int], - # Kafka information - group_id: Optional[str], - listener: Optional["ConsumerRebalanceListener"], - pattern: Optional[str], - connection_args: "AnyDict", - partitions: Collection["TopicPartition"], - is_manual: bool, - # Subscriber args - max_workers: int, - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[Tuple[ConsumerRecord, ...]]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, -) -> "AsyncAPIBatchSubscriber": ... - - -@overload -def create_subscriber( - *topics: str, - batch: Literal[False], - batch_timeout_ms: int, - max_records: Optional[int], - # Kafka information - group_id: Optional[str], - listener: Optional["ConsumerRebalanceListener"], - pattern: Optional[str], - connection_args: "AnyDict", - partitions: Collection["TopicPartition"], - is_manual: bool, - # Subscriber args - max_workers: int, - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[ConsumerRecord]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, -) -> Union[ - "AsyncAPIDefaultSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", -]: ... - - -@overload -def create_subscriber( - *topics: str, - batch: bool, - batch_timeout_ms: int, - max_records: Optional[int], - # Kafka information - group_id: Optional[str], - listener: Optional["ConsumerRebalanceListener"], - pattern: Optional[str], - connection_args: "AnyDict", - partitions: Collection["TopicPartition"], - is_manual: bool, - # Subscriber args - max_workers: int, - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence[ - "BrokerMiddleware[Union[ConsumerRecord, Tuple[ConsumerRecord, ...]]]" - ], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, -) -> Union[ - "AsyncAPIDefaultSubscriber", - "AsyncAPIBatchSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", -]: ... + from faststream._internal.basic_types import AnyDict + from faststream.kafka.configs import KafkaBrokerConfig def create_subscriber( *topics: str, batch: bool, batch_timeout_ms: int, - max_records: Optional[int], + max_records: int | None, # Kafka information - group_id: Optional[str], + group_id: str | None, listener: Optional["ConsumerRebalanceListener"], - pattern: Optional[str], + pattern: str | None, connection_args: "AnyDict", partitions: Collection["TopicPartition"], - is_manual: bool, + auto_commit: bool, # Subscriber args + ack_policy: "AckPolicy", max_workers: int, no_ack: bool, no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence[ - "BrokerMiddleware[Union[ConsumerRecord, Tuple[ConsumerRecord, ...]]]" - ], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], + config: "KafkaBrokerConfig", + # Specification args + title_: str | None, + description_: str | None, include_in_schema: bool, ) -> Union[ - "AsyncAPIDefaultSubscriber", - "AsyncAPIBatchSubscriber", - "AsyncAPIConcurrentDefaultSubscriber", - "AsyncAPIConcurrentBetweenPartitionsSubscriber", + "DefaultSubscriber", + "BatchSubscriber", + "ConcurrentDefaultSubscriber", + "ConcurrentBetweenPartitionsSubscriber", ]: - if is_manual and not group_id: - raise SetupError("You must use `group_id` with manual commit mode.") - - if is_manual and max_workers > 1: - if len(topics) > 1: - raise SetupError( - "You must use a single topic with concurrent manual commit mode." - ) - if pattern is not None: - raise SetupError( - "You can not use a pattern with concurrent manual commit mode." - ) - if partitions: - raise SetupError( - "Manual partition assignment is not supported with concurrent manual commit mode." - ) - - if not topics and not partitions and not pattern: - raise SetupError( - "You should provide either `topics` or `partitions` or `pattern`." - ) - elif topics and partitions: - raise SetupError("You can't provide both `topics` and `partitions`.") - elif topics and pattern: - raise SetupError("You can't provide both `topics` and `pattern`.") - elif partitions and pattern: - raise SetupError("You can't provide both `partitions` and `pattern`.") - - if batch: - return AsyncAPIBatchSubscriber( - *topics, - batch_timeout_ms=batch_timeout_ms, - max_records=max_records, - group_id=group_id, - listener=listener, - pattern=pattern, - connection_args=connection_args, + _validate_input_for_misconfigure( + *topics, + pattern=pattern, + partitions=partitions, + ack_policy=ack_policy, + no_ack=no_ack, + auto_commit=auto_commit, + max_workers=max_workers, + ) + + subscriber_config = KafkaSubscriberConfig( + topics=topics, + partitions=partitions, + connection_args=connection_args, + group_id=group_id, + listener=listener, + pattern=pattern, + no_reply=no_reply, + _outer_config=config, + _ack_policy=ack_policy, + # deprecated options to remove in 0.7.0 + _auto_commit=auto_commit, + _no_ack=no_ack, + ) + + calls = CallsCollection() + + specification = KafkaSubscriberSpecification( + _outer_config=config, + calls=calls, + specification_config=KafkaSubscriberSpecificationConfig( + topics=topics, partitions=partitions, - is_manual=is_manual, - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, title_=title_, description_=description_, include_in_schema=include_in_schema, + ), + ) + + if batch: + return BatchSubscriber( + subscriber_config, + specification, + calls, + batch_timeout_ms=batch_timeout_ms, + max_records=max_records, ) - else: - if max_workers > 1: - if is_manual: - return AsyncAPIConcurrentBetweenPartitionsSubscriber( - topics[0], - max_workers=max_workers, - group_id=group_id, - listener=listener, - pattern=pattern, - connection_args=connection_args, - partitions=partitions, - is_manual=is_manual, - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - else: - return AsyncAPIConcurrentDefaultSubscriber( - *topics, - max_workers=max_workers, - group_id=group_id, - listener=listener, - pattern=pattern, - connection_args=connection_args, - partitions=partitions, - is_manual=is_manual, - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - else: - return AsyncAPIDefaultSubscriber( - *topics, - group_id=group_id, - listener=listener, - pattern=pattern, - connection_args=connection_args, - partitions=partitions, - is_manual=is_manual, - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, + if max_workers > 1: + if subscriber_config.ack_first: + return ConcurrentDefaultSubscriber( + subscriber_config, + specification, + calls, + max_workers=max_workers, ) + subscriber_config.topics = (topics[0],) + return ConcurrentBetweenPartitionsSubscriber( + subscriber_config, + specification, + calls, + max_workers=max_workers, + ) -@overload -def create_publisher( - *, - batch: Literal[True], - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[Tuple[ConsumerRecord, ...]]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - autoflush: bool = False, -) -> "AsyncAPIBatchPublisher": ... + return DefaultSubscriber(subscriber_config, specification, calls) -@overload -def create_publisher( - *, - batch: Literal[False], - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[ConsumerRecord]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - autoflush: bool = False, -) -> "AsyncAPIDefaultPublisher": ... +def _validate_input_for_misconfigure( + *topics: str, + ack_policy: "AckPolicy", + auto_commit: bool, + no_ack: bool, + max_workers: int, + pattern: str | None, + partitions: Iterable["TopicPartition"], +) -> None: + if auto_commit is not EMPTY: + warnings.warn( + "`auto_commit` option was deprecated in prior to `ack_policy=AckPolicy.ACK_FIRST`. Scheduled to remove in 0.7.0", + category=DeprecationWarning, + stacklevel=4, + ) + if ack_policy is not EMPTY: + msg = "You can't use deprecated `auto_commit` and `ack_policy` simultaneously. Please, use `ack_policy` only." + raise SetupError(msg) -@overload -def create_publisher( - *, - batch: bool, - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence[ - "BrokerMiddleware[Union[Tuple[ConsumerRecord, ...], ConsumerRecord]]" - ], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - autoflush: bool = False, -) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", -]: ... + ack_policy = AckPolicy.ACK_FIRST if auto_commit else AckPolicy.REJECT_ON_ERROR + if no_ack is not EMPTY: + warnings.warn( + "`no_ack` option was deprecated in prior to `ack_policy=AckPolicy.DO_NOTHING`. Scheduled to remove in 0.7.0", + category=DeprecationWarning, + stacklevel=4, + ) -def create_publisher( - *, - batch: bool, - key: Optional[bytes], - topic: str, - partition: Optional[int], - headers: Optional[Dict[str, str]], - reply_to: str, - # Publisher args - broker_middlewares: Sequence[ - "BrokerMiddleware[Union[Tuple[ConsumerRecord, ...], ConsumerRecord]]" - ], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - autoflush: bool = False, -) -> Union[ - "AsyncAPIBatchPublisher", - "AsyncAPIDefaultPublisher", -]: - publisher: Union[AsyncAPIBatchPublisher, AsyncAPIDefaultPublisher] - if batch: - if key: - raise SetupError("You can't setup `key` with batch publisher") + if ack_policy is not EMPTY: + msg = "You can't use deprecated `no_ack` and `ack_policy` simultaneously. Please, use `ack_policy` only." + raise SetupError(msg) - publisher = AsyncAPIBatchPublisher( - topic=topic, - partition=partition, - headers=headers, - reply_to=reply_to, - broker_middlewares=broker_middlewares, - middlewares=middlewares, - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - else: - publisher = AsyncAPIDefaultPublisher( - key=key, - # basic args - topic=topic, - partition=partition, - headers=headers, - reply_to=reply_to, - broker_middlewares=broker_middlewares, - middlewares=middlewares, - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + ack_policy = AckPolicy.DO_NOTHING if no_ack else EMPTY + + if ack_policy is EMPTY: + ack_policy = AckPolicy.ACK_FIRST + + if max_workers > 1 and ack_policy is not AckPolicy.ACK_FIRST: + if len(topics) > 1: + msg = "You must use a single topic with concurrent manual commit mode." + raise SetupError(msg) + + if pattern is not None: + msg = "You can not use a pattern with concurrent manual commit mode." + raise SetupError(msg) - if autoflush: - default_publish: Callable[..., Awaitable[Optional[Any]]] = publisher.publish + if partitions: + msg = "Manual partition assignment is not supported with concurrent manual commit mode." + raise SetupError(msg) + + if not topics and not partitions and not pattern: + msg = "You should provide either `topics` or `partitions` or `pattern`." + raise SetupError(msg) - @wraps(default_publish) - async def autoflush_wrapper(*args: Any, **kwargs: Any) -> Optional[Any]: - result = await default_publish(*args, **kwargs) - await publisher.flush() - return result + if topics and partitions: + msg = "You can't provide both `topics` and `partitions`." + raise SetupError(msg) - publisher.publish = autoflush_wrapper # type: ignore[method-assign] + if topics and pattern: + msg = "You can't provide both `topics` and `pattern`." + raise SetupError(msg) - return publisher + if partitions and pattern: + msg = "You can't provide both `partitions` and `pattern`." + raise SetupError(msg) diff --git a/faststream/kafka/subscriber/specification.py b/faststream/kafka/subscriber/specification.py new file mode 100644 index 0000000000..b9eb83ad33 --- /dev/null +++ b/faststream/kafka/subscriber/specification.py @@ -0,0 +1,54 @@ + +from faststream._internal.endpoint.subscriber import SubscriberSpecification +from faststream.kafka.configs import KafkaBrokerConfig +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 .config import KafkaSubscriberSpecificationConfig + + +class KafkaSubscriberSpecification(SubscriberSpecification[KafkaBrokerConfig, KafkaSubscriberSpecificationConfig]): + @property + def topics(self) -> list[str]: + topics: set[str] = set() + + topics.update(f"{self._outer_config.prefix}{t}" for t in self.config.topics) + + topics.update(f"{self._outer_config.prefix}{p.topic}" for p in self.config.partitions) + + return list(topics) + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{','.join(self.topics)}:{self.call_name}" + + def get_schema(self) -> dict[str, SubscriberSpec]: + payloads = self.get_payloads() + + channels = {} + for t in self.topics: + handler_name = self.config.title_ or f"{t}:{self.call_name}" + + channels[handler_name] = SubscriberSpec( + description=self.description, + operation=Operation( + message=Message( + title=f"{handler_name}:Message", + payload=resolve_payloads(payloads), + ), + bindings=None, + ), + bindings=ChannelBinding( + kafka=kafka.ChannelBinding( + topic=t, + partitions=None, + replicas=None, + ), + ), + ) + + return channels diff --git a/faststream/kafka/subscriber/usecase.py b/faststream/kafka/subscriber/usecase.py index c3d5e30cb0..3020776d2a 100644 --- a/faststream/kafka/subscriber/usecase.py +++ b/faststream/kafka/subscriber/usecase.py @@ -1,151 +1,95 @@ import logging -from abc import ABC, abstractmethod +from abc import abstractmethod +from collections.abc import AsyncIterator, Callable, Iterable, Sequence from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, - Optional, - Sequence, - Tuple, - cast, -) +from typing import TYPE_CHECKING, Any, Optional, cast import anyio from aiokafka import ConsumerRecord, TopicPartition from aiokafka.errors import ConsumerStoppedError, KafkaError, UnsupportedCodecError from typing_extensions import override -from faststream.broker.publisher.fake import FakePublisher -from faststream.broker.subscriber.mixins import ConcurrentMixin, TasksMixin -from faststream.broker.subscriber.usecase import SubscriberUsecase -from faststream.broker.types import ( - AsyncCallable, - BrokerMiddleware, - CustomCallable, - MsgType, -) -from faststream.broker.utils import process_msg -from faststream.kafka.listener import LoggingListenerProxy +from faststream._internal.endpoint.subscriber.mixins import ConcurrentMixin, TasksMixin +from faststream._internal.endpoint.subscriber.usecase import SubscriberUsecase +from faststream._internal.endpoint.utils import process_msg +from faststream._internal.types import MsgType +from faststream._internal.utils.path import compile_path +from faststream.kafka.listener import make_logging_listener from faststream.kafka.message import KafkaAckableMessage, KafkaMessage, KafkaRawMessage from faststream.kafka.parser import AioKafkaBatchParser, AioKafkaParser -from faststream.utils.path import compile_path +from faststream.kafka.publisher.fake import KafkaFakePublisher if TYPE_CHECKING: from aiokafka import AIOKafkaConsumer - from aiokafka.abc import ConsumerRebalanceListener - from fast_depends.dependencies import Depends - from faststream.broker.message import StreamMessage - from faststream.broker.publisher.proto import ProducerProto - from faststream.types import AnyDict, Decorator, LoggerProto + from faststream._internal.endpoint.publisher import BasePublisherProto + from faststream._internal.endpoint.subscriber.call_item import CallsCollection + from faststream.kafka.configs import KafkaBrokerConfig + from faststream.message import StreamMessage + from .config import KafkaSubscriberConfig + from .specification import KafkaSubscriberSpecification -class LogicSubscriber(ABC, TasksMixin, SubscriberUsecase[MsgType]): - """A class to handle logic for consuming messages from Kafka.""" - topics: Sequence[str] - group_id: Optional[str] +class LogicSubscriber(TasksMixin, SubscriberUsecase[MsgType]): + """A class to handle logic for consuming messages from Kafka.""" - builder: Optional[Callable[..., "AIOKafkaConsumer"]] consumer: Optional["AIOKafkaConsumer"] - client_id: Optional[str] batch: bool + parser: AioKafkaParser + + _outer_config: "KafkaBrokerConfig" def __init__( self, - *topics: str, - # Kafka information - group_id: Optional[str], - connection_args: "AnyDict", - listener: Optional["ConsumerRebalanceListener"], - pattern: Optional[str], - partitions: Iterable["TopicPartition"], - # Subscriber args - default_parser: "AsyncCallable", - default_decoder: "AsyncCallable", - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[MsgType]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, + config: "KafkaSubscriberConfig", + specification: "KafkaSubscriberSpecification", + calls: "CallsCollection[MsgType]", ) -> None: - super().__init__( - default_parser=default_parser, - default_decoder=default_decoder, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - self.topics = topics - self.partitions = partitions - self.group_id = group_id + super().__init__(config, specification, calls) - self._pattern = pattern - self._listener = listener - self._connection_args = connection_args + self._topics = config.topics + self._partitions = config.partitions + self.group_id = config.group_id - # Setup it later - self.client_id = "" - self.builder = None + self._pattern = config.pattern + self._listener = config.listener + self._connection_args = config.connection_args self.consumer = None - @override - def setup( # type: ignore[override] - self, - *, - client_id: Optional[str], - builder: Callable[..., "AIOKafkaConsumer"], - # basic args - logger: Optional["LoggerProto"], - producer: Optional["ProducerProto"], - graceful_timeout: Optional[float], - extra_context: "AnyDict", - # broker options - broker_parser: Optional["CustomCallable"], - broker_decoder: Optional["CustomCallable"], - # dependant args - apply_types: bool, - is_validate: bool, - _get_dependant: Optional[Callable[..., Any]], - _call_decorators: Iterable["Decorator"], - ) -> None: - self.client_id = client_id - self.builder = builder - - super().setup( - logger=logger, - producer=producer, - graceful_timeout=graceful_timeout, - extra_context=extra_context, - broker_parser=broker_parser, - broker_decoder=broker_decoder, - apply_types=apply_types, - is_validate=is_validate, - _get_dependant=_get_dependant, - _call_decorators=_call_decorators, - ) + @property + def pattern(self) -> str | None: + if not self._pattern: + return self._pattern + return f"{self._outer_config.prefix}{self._pattern}" + + @property + def topics(self) -> list[str]: + return [f"{self._outer_config.prefix}{t}" for t in self._topics] + + @property + def partitions(self) -> list[TopicPartition]: + return [ + TopicPartition( + topic=f"{self._outer_config.prefix}{p.topic}", + partition=p.partition, + ) + for p in self._partitions + ] + + @property + def builder(self) -> Callable[..., "AIOKafkaConsumer"]: + return self._outer_config.builder + + @property + def client_id(self) -> str | None: + return self._outer_config.client_id async def start(self) -> None: """Start the consumer.""" - assert self.builder, "You should setup subscriber at first." # nosec B101 + await super().start() self.consumer = consumer = self.builder( group_id=self.group_id, @@ -153,12 +97,17 @@ async def start(self) -> None: **self._connection_args, ) - if self.topics or self._pattern: + self.parser._setup(consumer) + + if self.topics or self.pattern: consumer.subscribe( topics=self.topics, - pattern=self._pattern, - listener=LoggingListenerProxy( - consumer=consumer, logger=self.logger, listener=self._listener + pattern=self.pattern, + listener=make_logging_listener( + consumer=consumer, + logger=self._outer_config.logger.logger.logger, + log_extra=self.get_log_context(None), + listener=self._listener, ), ) @@ -166,7 +115,8 @@ async def start(self) -> None: consumer.assign(partitions=self.partitions) await consumer.start() - await super().start() + + self._post_start() if self.calls: self.add_task(self._run_consume_loop(self.consumer)) @@ -183,14 +133,16 @@ async def get_one( self, *, timeout: float = 5.0, - ) -> "Optional[StreamMessage[MsgType]]": - assert self.consumer, "You should start subscriber at first." # nosec B101 + ) -> "StreamMessage[MsgType] | None": assert ( # nosec B101 not self.calls ), "You can't use `get_one` method if subscriber has registered handlers." + assert self.consumer, "You should start subscriber at first." # nosec B101 + raw_messages = await self.consumer.getmany( - timeout_ms=timeout * 1000, max_records=1 + timeout_ms=timeout * 1000, + max_records=1, ) if not raw_messages: @@ -198,32 +150,50 @@ async def get_one( ((raw_message,),) = raw_messages.values() + context = self._outer_config.fd_config.context + return await process_msg( msg=raw_message, - middlewares=self._broker_middlewares, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), parser=self._parser, decoder=self._decoder, ) + @override + async def __aiter__(self) -> AsyncIterator["StreamMessage[MsgType]"]: # type: ignore[override] + assert self.consumer, "You should start subscriber at first." # nosec B101 + assert ( # nosec B101 + not self.calls + ), "You can't use `get_one` method if subscriber has registered handlers." + + async for raw_message in self.consumer: + context = self._outer_config.fd_config.context + msg: StreamMessage[MsgType] = await process_msg( # type: ignore[assignment] + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + yield msg + def _make_response_publisher( self, message: "StreamMessage[Any]", - ) -> Sequence[FakePublisher]: - if self._producer is None: - return () - + ) -> Sequence["BasePublisherProto"]: return ( - FakePublisher( - self._producer.publish, - publish_kwargs={ - "topic": message.reply_to, - }, + KafkaFakePublisher( + self._outer_config.producer, + topic=message.reply_to, ), ) @abstractmethod async def get_msg(self, consumer: "AIOKafkaConsumer") -> MsgType: - raise NotImplementedError() + raise NotImplementedError async def _run_consume_loop(self, consumer: "AIOKafkaConsumer") -> None: assert consumer, "You should start subscriber at first." # nosec B101 @@ -245,8 +215,10 @@ async def _run_consume_loop(self, consumer: "AIOKafkaConsumer") -> None: except KafkaError as e: self._log(logging.ERROR, "Kafka error occurred", exc_info=e) + if connected: connected = False + await anyio.sleep(5) except ConsumerStoppedError: @@ -262,110 +234,57 @@ async def _run_consume_loop(self, consumer: "AIOKafkaConsumer") -> None: async def consume_one(self, msg: MsgType) -> None: await self.consume(msg) - @staticmethod - def get_routing_hash( - topics: Iterable[str], - group_id: Optional[str] = None, - ) -> int: - return hash("".join((*topics, group_id or ""))) - @property - def topic_names(self) -> List[str]: - if self._pattern: - return [self._pattern] + def topic_names(self) -> list[str]: + if self.pattern: + topics: Iterable[str] = [self.pattern] + elif self.topics: - return list(self.topics) + topics = self.topics + else: - return [f"{p.topic}-{p.partition}" for p in self.partitions] + topics = (f"{p.topic}-{p.partition}" for p in self.partitions) - def __hash__(self) -> int: - return self.get_routing_hash( - topics=self.topic_names, - group_id=self.group_id, - ) + return [f"{self._outer_config.prefix}{t}" for t in topics] @staticmethod def build_log_context( message: Optional["StreamMessage[Any]"], topic: str, - group_id: Optional[str] = None, - ) -> Dict[str, str]: + group_id: str | None = None, + ) -> dict[str, str]: return { "topic": topic, "group_id": group_id or "", "message_id": getattr(message, "message_id", ""), } - def add_prefix(self, prefix: str) -> None: - self.topics = tuple("".join((prefix, t)) for t in self.topics) - - self.partitions = [ - TopicPartition( - topic="".join((prefix, p.topic)), - partition=p.partition, - ) - for p in self.partitions - ] - class DefaultSubscriber(LogicSubscriber["ConsumerRecord"]): def __init__( self, - *topics: str, - # Kafka information - group_id: Optional[str], - listener: Optional["ConsumerRebalanceListener"], - pattern: Optional[str], - connection_args: "AnyDict", - partitions: Iterable["TopicPartition"], - is_manual: bool, - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[ConsumerRecord]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, + config: "KafkaSubscriberConfig", + specification: "KafkaSubscriberSpecification", + calls: "CallsCollection", ) -> None: - if pattern: + if config.pattern: reg, pattern = compile_path( - pattern, + config.pattern, replace_symbol=".*", patch_regex=lambda x: x.replace(r"\*", ".*"), ) + config.pattern = pattern else: reg = None - parser = AioKafkaParser( - msg_class=KafkaAckableMessage if is_manual else KafkaMessage, + self.parser = AioKafkaParser( + msg_class=KafkaMessage if config.ack_first else KafkaAckableMessage, regex=reg, ) - - super().__init__( - *topics, - group_id=group_id, - listener=listener, - pattern=pattern, - connection_args=connection_args, - partitions=partitions, - # subscriber args - default_parser=parser.parse_message, - default_decoder=parser.decode_message, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + config.parser = self.parser.parse_message + config.decoder = self.parser.decode_message + super().__init__(config, specification, calls) async def get_msg(self, consumer: "AIOKafkaConsumer") -> "ConsumerRecord": assert consumer, "You should setup subscriber at first." # nosec B101 @@ -374,7 +293,7 @@ async def get_msg(self, consumer: "AIOKafkaConsumer") -> "ConsumerRecord": def get_log_context( self, message: Optional["StreamMessage[ConsumerRecord]"], - ) -> Dict[str, str]: + ) -> dict[str, str]: if message is None: topic = ",".join(self.topic_names) else: @@ -387,75 +306,40 @@ def get_log_context( ) -class BatchSubscriber(LogicSubscriber[Tuple["ConsumerRecord", ...]]): +class BatchSubscriber(LogicSubscriber[tuple["ConsumerRecord", ...]]): def __init__( self, - *topics: str, + config: "KafkaSubscriberConfig", + specification: "KafkaSubscriberSpecification", + calls: "CallsCollection", batch_timeout_ms: int, - max_records: Optional[int], - # Kafka information - group_id: Optional[str], - listener: Optional["ConsumerRebalanceListener"], - pattern: Optional[str], - connection_args: "AnyDict", - partitions: Iterable["TopicPartition"], - is_manual: bool, - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence[ - "BrokerMiddleware[Sequence[Tuple[ConsumerRecord, ...]]]" - ], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, + max_records: int | None, ) -> None: - self.batch_timeout_ms = batch_timeout_ms - self.max_records = max_records - - if pattern: + if config.pattern: reg, pattern = compile_path( - pattern, + config.pattern, replace_symbol=".*", patch_regex=lambda x: x.replace(r"\*", ".*"), ) + config.pattern = pattern else: reg = None - parser = AioKafkaBatchParser( - msg_class=KafkaAckableMessage if is_manual else KafkaMessage, + self.parser = AioKafkaBatchParser( + msg_class=KafkaMessage if config.ack_first else KafkaAckableMessage, regex=reg, ) + config.decoder = self.parser.decode_message + config.parser = self.parser.parse_message + super().__init__(config, specification, calls) - super().__init__( - *topics, - group_id=group_id, - listener=listener, - pattern=pattern, - connection_args=connection_args, - partitions=partitions, - # subscriber args - default_parser=parser.parse_message, - default_decoder=parser.decode_message, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + self.batch_timeout_ms = batch_timeout_ms + self.max_records = max_records async def get_msg( self, consumer: "AIOKafkaConsumer" - ) -> Tuple["ConsumerRecord", ...]: + ) -> tuple["ConsumerRecord", ...]: assert consumer, "You should setup subscriber at first." # nosec B101 messages = await consumer.getmany( @@ -471,8 +355,8 @@ async def get_msg( def get_log_context( self, - message: Optional["StreamMessage[Tuple[ConsumerRecord, ...]]"], - ) -> Dict[str, str]: + message: Optional["StreamMessage[tuple[ConsumerRecord, ...]]"], + ) -> dict[str, str]: if message is None: topic = ",".join(self.topic_names) else: @@ -485,50 +369,7 @@ def get_log_context( ) -class ConcurrentDefaultSubscriber(ConcurrentMixin[ConsumerRecord], DefaultSubscriber): - def __init__( - self, - *topics: str, - # Kafka information - group_id: Optional[str], - listener: Optional["ConsumerRebalanceListener"], - pattern: Optional[str], - connection_args: "AnyDict", - partitions: Iterable["TopicPartition"], - is_manual: bool, - # Subscriber args - max_workers: int, - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[ConsumerRecord]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - super().__init__( - *topics, - group_id=group_id, - listener=listener, - pattern=pattern, - connection_args=connection_args, - partitions=partitions, - is_manual=is_manual, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - max_workers=max_workers, - ) - +class ConcurrentDefaultSubscriber(ConcurrentMixin["ConsumerRecord"], DefaultSubscriber): async def start(self) -> None: await super().start() self.start_consume_task() @@ -538,84 +379,65 @@ async def consume_one(self, msg: "ConsumerRecord") -> None: class ConcurrentBetweenPartitionsSubscriber(DefaultSubscriber): - consumer_subgroup: Iterable["AIOKafkaConsumer"] - topics: str + consumer_subgroup: list["AIOKafkaConsumer"] def __init__( self, - topic: str, - # Kafka information - group_id: Optional[str], - listener: Optional["ConsumerRebalanceListener"], - pattern: Optional[str], - connection_args: "AnyDict", - partitions: Iterable["TopicPartition"], - is_manual: bool, - # Subscriber args + config: "KafkaSubscriberConfig", + specification: "KafkaSubscriberSpecification", + calls: "CallsCollection", max_workers: int, - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[ConsumerRecord]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, ) -> None: - super().__init__( - topic, - group_id=group_id, - listener=listener, - pattern=pattern, - connection_args=connection_args, - partitions=partitions, - is_manual=is_manual, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + super().__init__(config, specification, calls) + self.max_workers = max_workers + self.consumer_subgroup = [] async def start(self) -> None: """Start the consumer subgroup.""" - assert self.builder, "You should setup subscriber at first." # nosec B101 + await super(LogicSubscriber, self).start() + + if self.calls: + self.consumer_subgroup = [ + self.builder( + group_id=self.group_id, + client_id=self.client_id, + **self._connection_args, + ) + for _ in range(self.max_workers) + ] - self.consumer_subgroup = [ - self.builder( + else: + # We should create single consumer to support + # `get_one()` and `__aiter__` methods + self.consumer = self.builder( group_id=self.group_id, client_id=self.client_id, **self._connection_args, ) - for _ in range(self.max_workers) - ] - - [ - consumer.subscribe( - topics=self.topics, - listener=LoggingListenerProxy( - consumer=consumer, logger=self.logger, listener=self._listener - ), - ) - for consumer in self.consumer_subgroup - ] + self.consumer_subgroup = [self.consumer] + # Subscribers starting should be called concurrently + # to balance them correctly async with anyio.create_task_group() as tg: - for consumer in self.consumer_subgroup: - tg.start_soon(consumer.start) + for c in self.consumer_subgroup: + c.subscribe( + topics=self.topics, + listener=make_logging_listener( + consumer=c, + logger=self._outer_config.logger.logger.logger, + log_extra=self.get_log_context(None), + listener=self._listener, + ), + ) + + tg.start_soon(c.start) - self.running = True + self._post_start() if self.calls: - for consumer in self.consumer_subgroup: - self.add_task(self._run_consume_loop(consumer)) + for c in self.consumer_subgroup: + self.add_task(self._run_consume_loop(c)) async def close(self) -> None: if self.consumer_subgroup: diff --git a/faststream/kafka/testing.py b/faststream/kafka/testing.py index b179e84a65..668d80cadd 100755 --- a/faststream/kafka/testing.py +++ b/faststream/kafka/testing.py @@ -1,29 +1,33 @@ import re -from datetime import datetime -from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple +from collections.abc import Callable, Generator, Iterable, Iterator +from contextlib import ExitStack, contextmanager +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any, Optional from unittest.mock import AsyncMock, MagicMock import anyio from aiokafka import ConsumerRecord from typing_extensions import override -from faststream.broker.message import encode_message, gen_cor_id -from faststream.broker.utils import resolve_custom_func +from faststream._internal.endpoint.utils import resolve_custom_func +from faststream._internal.testing.broker import TestBroker, change_producer from faststream.exceptions import SubscriberNotFound from faststream.kafka import TopicPartition from faststream.kafka.broker import KafkaBroker from faststream.kafka.message import KafkaMessage from faststream.kafka.parser import AioKafkaParser -from faststream.kafka.publisher.asyncapi import AsyncAPIBatchPublisher from faststream.kafka.publisher.producer import AioKafkaFastProducer -from faststream.kafka.subscriber.asyncapi import AsyncAPIBatchSubscriber -from faststream.testing.broker import TestBroker -from faststream.utils.functions import timeout_scope +from faststream.kafka.publisher.usecase import BatchPublisher +from faststream.kafka.subscriber.usecase import BatchSubscriber +from faststream.message import encode_message, gen_cor_id if TYPE_CHECKING: - from faststream.kafka.publisher.asyncapi import AsyncAPIPublisher + from fast_depends.library.serializer import SerializerProto + + from faststream._internal.basic_types import SendableMessage + from faststream.kafka.publisher.usecase import LogicPublisher + from faststream.kafka.response import KafkaPublishCommand from faststream.kafka.subscriber.usecase import LogicSubscriber - from faststream.types import SendableMessage __all__ = ("TestKafkaBroker",) @@ -31,22 +35,36 @@ class TestKafkaBroker(TestBroker[KafkaBroker]): """A class to test Kafka brokers.""" + @contextmanager + def _patch_producer(self, broker: KafkaBroker) -> Iterator[None]: + fake_producer = FakeProducer(broker) + + with ExitStack() as es: + es.enter_context( + change_producer(broker.config.broker_config, fake_producer) + ) + yield + @staticmethod async def _fake_connect( # type: ignore[override] broker: KafkaBroker, *args: Any, **kwargs: Any, ) -> Callable[..., AsyncMock]: - broker._producer = FakeProducer(broker) + broker.config.broker_config._admin_client = AsyncMock() + + builder = MagicMock(return_value=FakeConsumer()) + broker.config.broker_config.builder = builder + return _fake_connection @staticmethod def create_publisher_fake_subscriber( broker: KafkaBroker, - publisher: "AsyncAPIPublisher[Any]", - ) -> Tuple["LogicSubscriber[Any]", bool]: - sub: Optional[LogicSubscriber[Any]] = None - for handler in broker._subscribers.values(): + publisher: "LogicPublisher[Any, Any]", + ) -> tuple["LogicSubscriber[Any]", bool]: + sub: LogicSubscriber[Any] | None = None + for handler in broker.subscribers: if _is_handler_matches(handler, publisher.topic, publisher.partition): sub = handler break @@ -56,16 +74,17 @@ def create_publisher_fake_subscriber( if publisher.partition: tp = TopicPartition( - topic=publisher.topic, partition=publisher.partition + topic=publisher.topic, + partition=publisher.partition, ) sub = broker.subscriber( partitions=[tp], - batch=isinstance(publisher, AsyncAPIBatchPublisher), + batch=isinstance(publisher, BatchPublisher), ) else: sub = broker.subscriber( publisher.topic, - batch=isinstance(publisher, AsyncAPIBatchPublisher), + batch=isinstance(publisher, BatchPublisher), ) else: is_real = True @@ -73,6 +92,17 @@ def create_publisher_fake_subscriber( return sub, is_real +class FakeConsumer: + async def start(self) -> None: + pass + + async def stop(self) -> None: + pass + + def subscribe(self, *args: Any, **kwargs: Any) -> None: + pass + + class FakeProducer(AioKafkaFastProducer): """A fake Kafka producer for testing purposes. @@ -90,126 +120,104 @@ def __init__(self, broker: KafkaBroker) -> None: self._parser = resolve_custom_func(broker._parser, default.parse_message) self._decoder = resolve_custom_func(broker._decoder, default.decode_message) + def __bool__(self) -> None: + return True + + @property + def closed(self) -> bool: + return False + @override async def publish( # type: ignore[override] self, - message: "SendableMessage", - topic: str, - key: Optional[bytes] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, - *, - reply_to: str = "", - rpc: bool = False, - rpc_timeout: Optional[float] = None, - raise_timeout: bool = False, - no_confirm: bool = False, - ) -> Optional[Any]: + cmd: "KafkaPublishCommand", + ) -> None: """Publish a message to the Kafka broker.""" incoming = build_message( - message=message, - topic=topic, - key=key, - partition=partition, - timestamp_ms=timestamp_ms, - headers=headers, - correlation_id=correlation_id, - reply_to=reply_to, + message=cmd.body, + topic=cmd.destination, + key=cmd.key, + partition=cmd.partition, + timestamp_ms=cmd.timestamp_ms, + headers=cmd.headers, + correlation_id=cmd.correlation_id, + reply_to=cmd.reply_to, + serializer=self.broker.config.fd_config._serializer ) - return_value = None + for handler in _find_handler( + self.broker.subscribers, + cmd.destination, + cmd.partition, + ): + msg_to_send = ( + [incoming] if isinstance(handler, BatchSubscriber) else incoming + ) - for handler in self.broker._subscribers.values(): # pragma: no branch - if _is_handler_matches(handler, topic, partition): - msg_to_send = ( - [incoming] - if isinstance(handler, AsyncAPIBatchSubscriber) - else incoming - ) - - with timeout_scope(rpc_timeout, raise_timeout): - response_msg = await self._execute_handler( - msg_to_send, topic, handler - ) - if rpc: - return_value = return_value or await self._decoder( - await self._parser(response_msg) - ) - - return return_value + await self._execute_handler(msg_to_send, cmd.destination, handler) @override async def request( # type: ignore[override] self, - message: "SendableMessage", - topic: str, - key: Optional[bytes] = None, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, - *, - timeout: Optional[float] = 0.5, + cmd: "KafkaPublishCommand", ) -> "ConsumerRecord": incoming = build_message( - message=message, - topic=topic, - key=key, - partition=partition, - timestamp_ms=timestamp_ms, - headers=headers, - correlation_id=correlation_id, + message=cmd.body, + topic=cmd.destination, + key=cmd.key, + partition=cmd.partition, + timestamp_ms=cmd.timestamp_ms, + headers=cmd.headers, + correlation_id=cmd.correlation_id, + serializer=self.broker.config.fd_config._serializer ) - for handler in self.broker._subscribers.values(): # pragma: no branch - if _is_handler_matches(handler, topic, partition): - msg_to_send = ( - [incoming] - if isinstance(handler, AsyncAPIBatchSubscriber) - else incoming + for handler in _find_handler( + self.broker.subscribers, + cmd.destination, + cmd.partition, + ): + msg_to_send = ( + [incoming] if isinstance(handler, BatchSubscriber) else incoming + ) + + with anyio.fail_after(cmd.timeout): + return await self._execute_handler( + msg_to_send, cmd.destination, handler ) - with anyio.fail_after(timeout): - return await self._execute_handler(msg_to_send, topic, handler) - raise SubscriberNotFound async def publish_batch( self, - *msgs: "SendableMessage", - topic: str, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - headers: Optional[Dict[str, str]] = None, - reply_to: str = "", - correlation_id: Optional[str] = None, - no_confirm: bool = False, + cmd: "KafkaPublishCommand", ) -> None: """Publish a batch of messages to the Kafka broker.""" - for handler in self.broker._subscribers.values(): # pragma: no branch - if _is_handler_matches(handler, topic, partition): - messages = ( - build_message( - message=message, - topic=topic, - partition=partition, - timestamp_ms=timestamp_ms, - headers=headers, - correlation_id=correlation_id, - reply_to=reply_to, - ) - for message in msgs + for handler in _find_handler( + self.broker.subscribers, + cmd.destination, + cmd.partition, + ): + messages = ( + build_message( + message=message, + topic=cmd.destination, + partition=cmd.partition, + timestamp_ms=cmd.timestamp_ms, + headers=cmd.headers, + correlation_id=cmd.correlation_id, + reply_to=cmd.reply_to, + serializer=self.broker.config.fd_config._serializer ) + for message in cmd.batch_bodies + ) - if isinstance(handler, AsyncAPIBatchSubscriber): - await self._execute_handler(list(messages), topic, handler) + if isinstance(handler, BatchSubscriber): + await self._execute_handler(list(messages), cmd.destination, handler) - else: - for m in messages: - await self._execute_handler(m, topic, handler) - return None + else: + for m in messages: + await self._execute_handler(m, cmd.destination, handler) async def _execute_handler( self, @@ -224,22 +232,24 @@ async def _execute_handler( message=result.body, headers=result.headers, correlation_id=result.correlation_id, + serializer=self.broker.config.fd_config._serializer ) def build_message( message: "SendableMessage", topic: str, - partition: Optional[int] = None, - timestamp_ms: Optional[int] = None, - key: Optional[bytes] = None, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, + partition: int | None = None, + timestamp_ms: int | None = None, + key: bytes | None = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, *, reply_to: str = "", + serializer: Optional["SerializerProto"] ) -> "ConsumerRecord": """Build a Kafka ConsumerRecord for a sendable message.""" - msg, content_type = encode_message(message) + msg, content_type = encode_message(message, serializer=serializer) k = key or b"" @@ -263,7 +273,7 @@ def build_message( offset=0, headers=[(i, j.encode()) for i, j in headers.items()], timestamp_type=1, - timestamp=timestamp_ms or int(datetime.now().timestamp() * 1000), + timestamp=timestamp_ms or int(datetime.now(timezone.utc).timestamp() * 1000), ) @@ -274,10 +284,26 @@ def _fake_connection(*args: Any, **kwargs: Any) -> AsyncMock: return mock +def _find_handler( + subscribers: Iterable["LogicSubscriber[Any]"], + topic: str, + partition: int | None, +) -> Generator["LogicSubscriber[Any]", None, None]: + published_groups = set() + for handler in subscribers: # pragma: no branch + if _is_handler_matches(handler, topic, partition): + if handler.group_id: + if handler.group_id in published_groups: + continue + else: + published_groups.add(handler.group_id) + yield handler + + def _is_handler_matches( handler: "LogicSubscriber[Any]", topic: str, - partition: Optional[int], + partition: int | None, ) -> bool: return bool( any( @@ -285,5 +311,5 @@ def _is_handler_matches( for p in handler.partitions ) or topic in handler.topics - or (handler._pattern and re.match(handler._pattern, topic)) + or (handler.pattern and re.match(handler.pattern, topic)), ) diff --git a/faststream/log/__init__.py b/faststream/log/__init__.py deleted file mode 100644 index 0fc7042279..0000000000 --- a/faststream/log/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from faststream.log.logging import logger - -__all__ = ("logger",) diff --git a/faststream/log/logging.py b/faststream/log/logging.py deleted file mode 100644 index 6384ab7608..0000000000 --- a/faststream/log/logging.py +++ /dev/null @@ -1,83 +0,0 @@ -import logging -import sys -from logging import LogRecord -from typing import Mapping - -from faststream.log.formatter import ColourizedFormatter -from faststream.utils.context.repository import context - -logger = logging.getLogger("faststream") -logger.setLevel(logging.INFO) -logger.propagate = False -main_handler = logging.StreamHandler(stream=sys.stderr) -main_handler.setFormatter( - ColourizedFormatter( - fmt="%(asctime)s %(levelname)8s - %(message)s", - use_colors=True, - ) -) -logger.addHandler(main_handler) - - -class ExtendedFilter(logging.Filter): - def __init__( - self, - default_context: Mapping[str, str], - message_id_ln: int, - name: str = "", - ) -> None: - self.default_context = default_context - self.message_id_ln = message_id_ln - super().__init__(name) - - def filter(self, record: LogRecord) -> bool: - if is_suitable := super().filter(record): - log_context: Mapping[str, str] = context.get_local( - "log_context", self.default_context - ) - - for k, v in log_context.items(): - value = getattr(record, k, v) - setattr(record, k, value) - - record.message_id = getattr(record, "message_id", "")[: self.message_id_ln] - - return is_suitable - - -def get_broker_logger( - name: str, - default_context: Mapping[str, str], - message_id_ln: int, -) -> logging.Logger: - logger = logging.getLogger(f"faststream.access.{name}") - logger.propagate = False - logger.addFilter(ExtendedFilter(default_context, message_id_ln)) - logger.setLevel(logging.INFO) - return logger - - -def _handler_exists(logger: logging.Logger) -> bool: - # Check if a StreamHandler for sys.stdout already exists in the logger. - for handler in logger.handlers: - if isinstance(handler, logging.StreamHandler) and handler.stream == sys.stdout: - return True - return False - - -def set_logger_fmt( - logger: logging.Logger, - fmt: str = "%(asctime)s %(levelname)s - %(message)s", -) -> None: - if _handler_exists(logger): - return - - handler = logging.StreamHandler(stream=sys.stdout) - - formatter = ColourizedFormatter( - fmt=fmt, - use_colors=True, - ) - handler.setFormatter(formatter) - - logger.addHandler(handler) diff --git a/faststream/message/__init__.py b/faststream/message/__init__.py new file mode 100644 index 0000000000..2dd53d6c4e --- /dev/null +++ b/faststream/message/__init__.py @@ -0,0 +1,12 @@ +from .message import AckStatus, StreamMessage +from .source_type import SourceType +from .utils import decode_message, encode_message, gen_cor_id + +__all__ = ( + "AckStatus", + "SourceType", + "StreamMessage", + "decode_message", + "encode_message", + "gen_cor_id", +) diff --git a/faststream/message/message.py b/faststream/message/message.py new file mode 100644 index 0000000000..ad65e4ee59 --- /dev/null +++ b/faststream/message/message.py @@ -0,0 +1,114 @@ +from enum import Enum +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Optional, + TypeVar, +) +from uuid import uuid4 + +from .source_type import SourceType + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict + from faststream._internal.types import AsyncCallable + +# prevent circular imports +MsgType = TypeVar("MsgType") + + +class AckStatus(str, Enum): + ACKED = "ACKED" + NACKED = "NACKED" + REJECTED = "REJECTED" + + +class StreamMessage(Generic[MsgType]): + """Generic class to represent a stream message.""" + + def __init__( + self, + raw_message: "MsgType", + body: bytes | Any, + *, + headers: Optional["AnyDict"] = None, + reply_to: str = "", + batch_headers: list["AnyDict"] | None = None, + path: Optional["AnyDict"] = None, + content_type: str | None = None, + correlation_id: str | None = None, + message_id: str | None = None, + source_type: SourceType = SourceType.CONSUME, + ) -> None: + self.raw_message = raw_message + self.body = body + self.reply_to = reply_to + self.content_type = content_type + self._source_type = source_type + + self.headers = headers or {} + self.batch_headers = batch_headers or [] + self.path = path or {} + self.correlation_id = correlation_id or str(uuid4()) + self.message_id = message_id or self.correlation_id + + self.committed: AckStatus | None = None + self.processed = False + + # Setup later + self.__decoder: AsyncCallable | None = None + self.__decoded_caches: dict[ + Any, Any + ] = {} # Cache values between filters and tests + + def set_decoder(self, decoder: "AsyncCallable") -> None: + self.__decoder = decoder + + def clear_cache(self) -> None: + self.__decoded_caches.clear() + + def __repr__(self) -> str: + inner = ", ".join( + filter( + bool, + ( + f"body={self.body!r}", + f"content_type={self.content_type}", + f"message_id={self.message_id}", + f"correlation_id={self.correlation_id}", + f"reply_to={self.reply_to}" if self.reply_to else "", + f"headers={self.headers}", + f"path={self.path}", + f"committed={self.committed}", + f"raw_message={self.raw_message}", + ), + ), + ) + + return f"{self.__class__.__name__}({inner})" + + async def decode(self) -> Optional["Any"]: + """Serialize the message by lazy decoder. + + Returns a cache after first usage. To prevent such behavior, please call + `message.clear_cache()` after `message.body` changes. + """ + assert self.__decoder, "You should call `set_decoder()` method first." # nosec B101 + + if (result := self.__decoded_caches.get(self.__decoder)) is None: + result = self.__decoded_caches[self.__decoder] = await self.__decoder(self) + + return result + + async def ack(self) -> None: + if self.committed is None: + self.committed = AckStatus.ACKED + + async def nack(self) -> None: + if self.committed is None: + self.committed = AckStatus.NACKED + + async def reject(self) -> None: + if self.committed is None: + self.committed = AckStatus.REJECTED diff --git a/faststream/message/source_type.py b/faststream/message/source_type.py new file mode 100644 index 0000000000..b6e4f95fd9 --- /dev/null +++ b/faststream/message/source_type.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class SourceType(str, Enum): + CONSUME = "CONSUME" + """Message consumed by basic subscriber flow.""" + + RESPONSE = "RESPONSE" + """RPC response consumed.""" diff --git a/faststream/message/utils.py b/faststream/message/utils.py new file mode 100644 index 0000000000..58a8a55e47 --- /dev/null +++ b/faststream/message/utils.py @@ -0,0 +1,76 @@ +import json +from collections.abc import Sequence +from contextlib import suppress +from typing import TYPE_CHECKING, Any, Optional, Union, cast +from uuid import uuid4 + +from faststream._internal._compat import json_dumps, json_loads +from faststream._internal.constants import ContentTypes + +if TYPE_CHECKING: + from fast_depends.library.serializer import SerializerProto + + from faststream._internal.basic_types import DecodedMessage, SendableMessage + + from .message import StreamMessage + + +def gen_cor_id() -> str: + """Generate random string to use as ID.""" + return str(uuid4()) + + +def decode_message(message: "StreamMessage[Any]") -> "DecodedMessage": + """Decodes a message.""" + body: Any = getattr(message, "body", message) + m: DecodedMessage = body + + if content_type := getattr(message, "content_type", False): + content_type = ContentTypes(cast("str", content_type)) + + if content_type is ContentTypes.TEXT: + m = body.decode() + + elif content_type is ContentTypes.JSON: + m = json_loads(body) + + else: + # content-type not set + with suppress(json.JSONDecodeError, UnicodeDecodeError): + m = json_loads(body) + + return m + + +def encode_message( + msg: Union[Sequence["SendableMessage"], "SendableMessage"], + serializer: Optional["SerializerProto"], +) -> tuple[bytes, str | None]: + """Encodes a message.""" + if msg is None: + return ( + b"", + None, + ) + + if isinstance(msg, bytes): + return ( + msg, + None, + ) + + if isinstance(msg, str): + return ( + msg.encode(), + ContentTypes.TEXT.value, + ) + + if serializer is not None: + return ( + serializer.encode(msg), + ContentTypes.JSON.value, + ) + return ( + json_dumps(msg), + ContentTypes.JSON.value, + ) diff --git a/faststream/middlewares/__init__.py b/faststream/middlewares/__init__.py new file mode 100644 index 0000000000..abf9fb4e62 --- /dev/null +++ b/faststream/middlewares/__init__.py @@ -0,0 +1,11 @@ +from faststream._internal.middlewares import BaseMiddleware +from faststream.middlewares.acknowledgement.conf import AckPolicy +from faststream.middlewares.acknowledgement.middleware import AcknowledgementMiddleware +from faststream.middlewares.exception import ExceptionMiddleware + +__all__ = ( + "AckPolicy", + "AcknowledgementMiddleware", + "BaseMiddleware", + "ExceptionMiddleware", +) diff --git a/faststream/cli/__init__.py b/faststream/middlewares/acknowledgement/__init__.py similarity index 100% rename from faststream/cli/__init__.py rename to faststream/middlewares/acknowledgement/__init__.py diff --git a/faststream/middlewares/acknowledgement/conf.py b/faststream/middlewares/acknowledgement/conf.py new file mode 100644 index 0000000000..c5cd759e10 --- /dev/null +++ b/faststream/middlewares/acknowledgement/conf.py @@ -0,0 +1,18 @@ +from enum import Enum + + +class AckPolicy(str, Enum): + ACK_FIRST = "ack_first" + """Ack message on consume.""" + + ACK = "ack" + """Ack message after all process.""" + + REJECT_ON_ERROR = "reject_on_error" + """Reject message on unhandled exceptions.""" + + NACK_ON_ERROR = "nack_on_error" + """Nack message on unhandled exceptions.""" + + DO_NOTHING = "do_nothing" + """Disable default FastStream Acknowledgement logic. User should confirm all actions manually.""" diff --git a/faststream/middlewares/acknowledgement/middleware.py b/faststream/middlewares/acknowledgement/middleware.py new file mode 100644 index 0000000000..cbd6381ff1 --- /dev/null +++ b/faststream/middlewares/acknowledgement/middleware.py @@ -0,0 +1,132 @@ +import logging +from typing import TYPE_CHECKING, Any, Optional + +from faststream._internal.middlewares import BaseMiddleware +from faststream.exceptions import ( + AckMessage, + HandlerException, + NackMessage, + RejectMessage, +) +from faststream.middlewares.acknowledgement.conf import AckPolicy + +if TYPE_CHECKING: + from types import TracebackType + + from faststream._internal.basic_types import AnyDict, AsyncFuncAny + from faststream._internal.context.repository import ContextRepo + from faststream._internal.di import LoggerState + from faststream.message import StreamMessage + + +class AcknowledgementMiddleware: + def __init__( + self, + logger: "LoggerState", + ack_policy: "AckPolicy", + extra_options: "AnyDict", + ) -> None: + self.ack_policy = ack_policy + self.extra_options = extra_options + self.logger = logger + + def __call__( + self, msg: Any | None, context: "ContextRepo" + ) -> "_AcknowledgementMiddleware": + return _AcknowledgementMiddleware( + msg, + logger=self.logger, + ack_policy=self.ack_policy, + extra_options=self.extra_options, + context=context, + ) + + +class _AcknowledgementMiddleware(BaseMiddleware): + def __init__( + self, + msg: Any | None, + /, + *, + logger: "LoggerState", + context: "ContextRepo", + extra_options: "AnyDict", + # can't be created with AckPolicy.DO_NOTHING + ack_policy: AckPolicy, + ) -> None: + super().__init__(msg, context=context) + + self.ack_policy = ack_policy + self.extra_options = extra_options + self.logger = logger + + self.message: StreamMessage[Any] | None = None + + async def consume_scope( + self, + call_next: "AsyncFuncAny", + msg: "StreamMessage[Any]", + ) -> Any: + self.message = msg + if self.ack_policy is AckPolicy.ACK_FIRST: + await self.__ack() + + return await call_next(msg) + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> bool | None: + if self.ack_policy is AckPolicy.ACK_FIRST: + return False + + if not exc_type: + await self.__ack() + + elif isinstance(exc_val, HandlerException): + if isinstance(exc_val, AckMessage): + await self.__ack(**exc_val.extra_options) + + elif isinstance(exc_val, NackMessage): + await self.__nack(**exc_val.extra_options) + + elif isinstance(exc_val, RejectMessage): # pragma: no branch + await self.__reject(**exc_val.extra_options) + + # Exception was processed and suppressed + return True + + elif self.ack_policy is AckPolicy.REJECT_ON_ERROR: + await self.__reject() + + elif self.ack_policy is AckPolicy.NACK_ON_ERROR: + await self.__nack() + + # Exception was not processed + return False + + async def __ack(self, **exc_extra_options: Any) -> None: + if self.message: + try: + await self.message.ack(**exc_extra_options, **self.extra_options) + except Exception as er: + if self.logger is not None: + self.logger.log(repr(er), logging.CRITICAL, exc_info=er) + + async def __nack(self, **exc_extra_options: Any) -> None: + if self.message: + try: + await self.message.nack(**exc_extra_options, **self.extra_options) + except Exception as er: + if self.logger is not None: + self.logger.log(repr(er), logging.CRITICAL, exc_info=er) + + async def __reject(self, **exc_extra_options: Any) -> None: + if self.message: + try: + await self.message.reject(**exc_extra_options, **self.extra_options) + except Exception as er: + if self.logger is not None: + self.logger.log(repr(er), logging.CRITICAL, exc_info=er) diff --git a/faststream/middlewares/exception.py b/faststream/middlewares/exception.py new file mode 100644 index 0000000000..521686e12b --- /dev/null +++ b/faststream/middlewares/exception.py @@ -0,0 +1,202 @@ +from collections.abc import Awaitable, Callable +from typing import ( + TYPE_CHECKING, + Any, + Literal, + NoReturn, + Optional, + TypeAlias, + cast, + overload, +) + +from faststream._internal.middlewares import BaseMiddleware +from faststream._internal.utils import apply_types +from faststream._internal.utils.functions import FakeContext, to_async +from faststream.exceptions import IgnoredException + +if TYPE_CHECKING: + from contextlib import AbstractContextManager + from types import TracebackType + + from faststream._internal.basic_types import AsyncFuncAny + from faststream._internal.context.repository import ContextRepo + from faststream.message import StreamMessage + + +GeneralExceptionHandler: TypeAlias = Callable[..., None] | Callable[..., Awaitable[None]] +PublishingExceptionHandler: TypeAlias = Callable[..., Any] + +CastedGeneralExceptionHandler: TypeAlias = Callable[..., Awaitable[None]] +CastedPublishingExceptionHandler: TypeAlias = Callable[..., Awaitable[Any]] +CastedHandlers: TypeAlias = list[ + tuple[ + type[Exception], + CastedGeneralExceptionHandler, + ] +] +CastedPublishingHandlers: TypeAlias = list[ + tuple[ + type[Exception], + CastedPublishingExceptionHandler, + ] +] + + +class ExceptionMiddleware: + __slots__ = ("_handlers", "_publish_handlers") + + _handlers: CastedHandlers + _publish_handlers: CastedPublishingHandlers + + def __init__( + self, + handlers: dict[type[Exception], GeneralExceptionHandler] | None = None, + publish_handlers: dict[type[Exception], PublishingExceptionHandler] | None = None, + ) -> None: + self._handlers: CastedHandlers = [ + (IgnoredException, ignore_handler), + *( + ( + exc_type, + apply_types( + cast("Callable[..., Awaitable[None]]", to_async(handler)), + ), + ) + for exc_type, handler in (handlers or {}).items() + ), + ] + + self._publish_handlers: CastedPublishingHandlers = [ + (IgnoredException, ignore_handler), + *( + (exc_type, apply_types(to_async(handler))) + for exc_type, handler in (publish_handlers or {}).items() + ), + ] + + @overload + def add_handler( + self, + exc: type[Exception], + publish: Literal[False] = False, + ) -> Callable[[GeneralExceptionHandler], GeneralExceptionHandler]: ... + + @overload + def add_handler( + self, + exc: type[Exception], + publish: Literal[True], + ) -> Callable[[PublishingExceptionHandler], PublishingExceptionHandler]: ... + + def add_handler( + self, + exc: type[Exception], + publish: bool = False, + ) -> Callable[[GeneralExceptionHandler], GeneralExceptionHandler] | Callable[[PublishingExceptionHandler], PublishingExceptionHandler]: + if publish: + + def pub_wrapper( + func: PublishingExceptionHandler, + ) -> PublishingExceptionHandler: + self._publish_handlers.append( + ( + exc, + apply_types(to_async(func)), + ), + ) + return func + + return pub_wrapper + + def default_wrapper( + func: GeneralExceptionHandler, + ) -> GeneralExceptionHandler: + self._handlers.append( + ( + exc, + apply_types(to_async(func)), + ), + ) + return func + + return default_wrapper + + def __call__( + self, + msg: Any | None, + /, + *, + context: "ContextRepo", + ) -> "_BaseExceptionMiddleware": + """Real middleware runtime constructor.""" + return _BaseExceptionMiddleware( + handlers=self._handlers, + publish_handlers=self._publish_handlers, + context=context, + msg=msg, + ) + + +class _BaseExceptionMiddleware(BaseMiddleware): + def __init__( + self, + *, + handlers: CastedHandlers, + publish_handlers: CastedPublishingHandlers, + context: "ContextRepo", + msg: Any | None, + ) -> None: + super().__init__(msg, context=context) + self._handlers = handlers + self._publish_handlers = publish_handlers + + async def consume_scope( + self, + call_next: "AsyncFuncAny", + msg: "StreamMessage[Any]", + ) -> Any: + try: + return await call_next(await self.on_consume(msg)) + + except Exception as exc: + exc_type = type(exc) + + for handler_type, handler in self._publish_handlers: + if issubclass(exc_type, handler_type): + return await handler(exc, context__=self.context) + + raise + + async def after_processed( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> bool | None: + if exc_type: + for handler_type, handler in self._handlers: + if issubclass(exc_type, handler_type): + # TODO: remove it after context will be moved to middleware + # In case parser/decoder error occurred + scope: AbstractContextManager[Any] + if not self.context.get_local("message"): + scope = self.context.scope("message", self.msg) + else: + scope = FakeContext() + + with scope: + await handler(exc_val, context__=self.context) + + return True + + return False + + return None + + +async def ignore_handler( + exception: IgnoredException, + **kwargs: Any, # suppress context +) -> NoReturn: + raise exception diff --git a/faststream/middlewares/logging.py b/faststream/middlewares/logging.py new file mode 100644 index 0000000000..986336696e --- /dev/null +++ b/faststream/middlewares/logging.py @@ -0,0 +1,94 @@ +from typing import TYPE_CHECKING, Any, Optional + +from faststream._internal.middlewares import BaseMiddleware +from faststream.exceptions import IgnoredException +from faststream.message.source_type import SourceType + +if TYPE_CHECKING: + from types import TracebackType + + from faststream._internal.basic_types import AsyncFuncAny + from faststream._internal.context.repository import ContextRepo + from faststream._internal.logger import LoggerState + from faststream.message import StreamMessage + + +class CriticalLogMiddleware: + def __init__(self, logger: "LoggerState") -> None: + """Initialize the class.""" + self.logger = logger + + def __call__( + self, + msg: Any | None, + /, + *, + context: "ContextRepo", + ) -> "_LoggingMiddleware": + return _LoggingMiddleware( + logger=self.logger, + msg=msg, + context=context, + ) + + +class _LoggingMiddleware(BaseMiddleware): + """A middleware class for logging critical errors.""" + + def __init__( + self, + *, + logger: "LoggerState", + context: "ContextRepo", + msg: Any | None, + ) -> None: + super().__init__(msg, context=context) + self.logger = logger + self._source_type = SourceType.CONSUME + + async def consume_scope( + self, + call_next: "AsyncFuncAny", + msg: "StreamMessage[Any]", + ) -> Any: + source_type = self._source_type = msg._source_type + + if source_type is not SourceType.RESPONSE: + self.logger.log( + "Received", + extra=self.context.get_local("log_context", {}), + ) + + return await call_next(msg) + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, + exc_tb: Optional["TracebackType"] = None, + ) -> bool: + """Asynchronously called after processing.""" + if self._source_type is not SourceType.RESPONSE: + c = self.context.get_local("log_context", {}) + + if exc_type: + # TODO: move critical logging to `subscriber.consume()` method + if issubclass(exc_type, IgnoredException): + self.logger.log( + message=str(exc_val), + extra=c, + ) + + else: + self.logger.log( + message=f"{exc_type.__name__}: {exc_val}", + exc_info=exc_val, + extra=c, + ) + + self.logger.log(message="Processed", extra=c) + + await super().__aexit__(exc_type, exc_val, exc_tb) + + # Exception was not processed + return False diff --git a/faststream/nats/__init__.py b/faststream/nats/__init__.py index 98339d8287..ba5459850d 100644 --- a/faststream/nats/__init__.py +++ b/faststream/nats/__init__.py @@ -1,3 +1,5 @@ +from faststream._internal.testing.app import TestApp + try: from nats.js.api import ( AckPolicy, @@ -14,16 +16,16 @@ StreamSource, ) - from faststream.testing.app import TestApp - from .annotations import NatsMessage - from .broker.broker import NatsBroker + from .broker import NatsBroker, NatsPublisher, NatsRoute, NatsRouter from .response import NatsResponse - from .router import NatsPublisher, NatsRoute, NatsRouter - from .schemas import JStream, KvWatch, ObjWatch, PullSub + from .schemas import JStream, KvWatch, ObjWatch, PubAck, PullSub from .testing import TestNatsBroker except ImportError as e: + if "'nats'" not in e.msg: + raise + from faststream.exceptions import INSTALL_FASTSTREAM_NATS raise ImportError(INSTALL_FASTSTREAM_NATS) from e @@ -46,6 +48,7 @@ "NatsRouter", "ObjWatch", "Placement", + "PubAck", "PullSub", "RePublish", "ReplayPolicy", diff --git a/faststream/nats/annotations.py b/faststream/nats/annotations.py index b93ba4e6e0..203784d9d8 100644 --- a/faststream/nats/annotations.py +++ b/faststream/nats/annotations.py @@ -1,15 +1,17 @@ +from typing import Annotated + from nats.aio.client import Client as _NatsClient from nats.js.client import JetStreamContext as _JetStream from nats.js.object_store import ObjectStore as _ObjectStore -from typing_extensions import Annotated -from faststream.annotations import ContextRepo, Logger, NoCast +from faststream._internal.context import Context +from faststream.annotations import ContextRepo, Logger from faststream.nats.broker import NatsBroker as _Broker from faststream.nats.message import NatsMessage as _Message -from faststream.nats.publisher.producer import NatsFastProducer as _CoreProducer -from faststream.nats.publisher.producer import NatsJSFastProducer as _JsProducer -from faststream.nats.subscriber.usecase import OBJECT_STORAGE_CONTEXT_KEY -from faststream.utils.context import Context +from faststream.nats.subscriber.usecases.object_storage_subscriber import ( + OBJECT_STORAGE_CONTEXT_KEY, +) +from faststream.params import NoCast __all__ = ( "Client", @@ -27,5 +29,3 @@ NatsBroker = Annotated[_Broker, Context("broker")] Client = Annotated[_NatsClient, Context("broker._connection")] JsClient = Annotated[_JetStream, Context("broker._stream")] -NatsProducer = Annotated[_CoreProducer, Context("broker._producer")] -NatsJsProducer = Annotated[_JsProducer, Context("broker._js_producer")] diff --git a/faststream/nats/broker/__init__.py b/faststream/nats/broker/__init__.py index 68408b4233..e4e7df6bfe 100644 --- a/faststream/nats/broker/__init__.py +++ b/faststream/nats/broker/__init__.py @@ -1,3 +1,4 @@ -from faststream.nats.broker.broker import NatsBroker +from .broker import NatsBroker +from .router import NatsPublisher, NatsRoute, NatsRouter -__all__ = ("NatsBroker",) +__all__ = ("NatsBroker", "NatsPublisher", "NatsRoute", "NatsRouter") diff --git a/faststream/nats/broker/broker.py b/faststream/nats/broker/broker.py index bb4e04588a..c0de595e66 100644 --- a/faststream/nats/broker/broker.py +++ b/faststream/nats/broker/broker.py @@ -1,15 +1,9 @@ import logging -import warnings +from collections.abc import Iterable, Sequence from typing import ( TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, + Annotated, Optional, - Sequence, - Type, Union, ) @@ -25,89 +19,95 @@ DEFAULT_PENDING_SIZE, DEFAULT_PING_INTERVAL, DEFAULT_RECONNECT_TIME_WAIT, + Client, ) +from nats.aio.msg import Msg from nats.errors import Error from nats.js.errors import BadRequestError -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import Doc, overload, override from faststream.__about__ import SERVICE_NAME -from faststream.broker.message import gen_cor_id -from faststream.nats.broker.logging import NatsLoggingBroker -from faststream.nats.broker.registrator import NatsRegistrator -from faststream.nats.helpers import KVBucketDeclarer, OSBucketDeclarer -from faststream.nats.publisher.producer import NatsFastProducer, NatsJSFastProducer +from faststream._internal.broker import BrokerUsecase +from faststream._internal.constants import EMPTY +from faststream._internal.di import FastDependsConfig +from faststream.message import gen_cor_id +from faststream.nats.configs import NatsBrokerConfig +from faststream.nats.publisher.producer import ( + NatsFastProducerImpl, + NatsJSFastProducer, +) +from faststream.nats.response import NatsPublishCommand from faststream.nats.security import parse_security -from faststream.nats.subscriber.asyncapi import AsyncAPISubscriber -from faststream.types import EMPTY +from faststream.nats.subscriber.usecases.basic import LogicSubscriber +from faststream.response.publish_type import PublishType +from faststream.specification.schema import BrokerSpec + +from .logging import make_nats_logger_state +from .registrator import NatsRegistrator if TYPE_CHECKING: - import ssl from types import TracebackType - from fast_depends.dependencies import Depends + from fast_depends.dependencies import Dependant + from fast_depends.library.serializer import SerializerProto from nats.aio.client import ( Callback, - Client, Credentials, ErrorCallback, JWTCallback, SignatureCallback, ) - from nats.aio.msg import Msg from nats.js.api import Placement, RePublish, StorageType - from nats.js.client import JetStreamContext from nats.js.kv import KeyValue from nats.js.object_store import ObjectStore - from typing_extensions import TypedDict, Unpack + from typing_extensions import TypedDict - from faststream.asyncapi import schema as asyncapi - from faststream.broker.publisher.proto import ProducerProto - from faststream.broker.types import ( + from faststream._internal.basic_types import ( + LoggerProto, + SendableMessage, + ) + from faststream._internal.broker.abc_broker import Registrator + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, ) from faststream.nats.message import NatsMessage - from faststream.nats.publisher.asyncapi import AsyncAPIPublisher + from faststream.nats.schemas import PubAck from faststream.security import BaseSecurity - from faststream.types import ( - AnyDict, - DecodedMessage, - Decorator, - LoggerProto, - SendableMessage, - ) + from faststream.specification.schema.extra import Tag, TagDict class NatsInitKwargs(TypedDict, total=False): """NatsBroker.connect() method type hints.""" error_cb: Annotated[ - Optional["ErrorCallback"], + "ErrorCallback" | None, Doc("Callback to report errors."), ] disconnected_cb: Annotated[ - Optional["Callback"], + "Callback" | None, Doc("Callback to report disconnection from NATS."), ] closed_cb: Annotated[ - Optional["Callback"], + "Callback" | None, Doc("Callback to report when client stops reconnection to NATS."), ] discovered_server_cb: Annotated[ - Optional["Callback"], + "Callback" | None, Doc("Callback to report when a new server joins the cluster."), ] reconnected_cb: Annotated[ - Optional["Callback"], Doc("Callback to report success reconnection.") + "Callback" | None, + Doc("Callback to report success reconnection."), ] name: Annotated[ - Optional[str], + str | None, Doc("Label the connection with name (shown in NATS monitoring)."), ] pedantic: Annotated[ bool, Doc( "Turn on NATS server pedantic mode that performs extra checks on the protocol. " - "https://docs.nats.io/using-nats/developer/connecting/misc#turn-on-pedantic-mode" + "https://docs.nats.io/using-nats/developer/connecting/misc#turn-on-pedantic-mode", ), ] verbose: Annotated[ @@ -141,34 +141,23 @@ class NatsInitKwargs(TypedDict, total=False): dont_randomize: Annotated[ bool, Doc( - "Boolean indicating should client randomly shuffle servers list for reconnection randomness." + "Boolean indicating should client randomly shuffle servers list for reconnection randomness.", ), ] flusher_queue_size: Annotated[ - int, Doc("Max count of commands awaiting to be flushed to the socket") + int, + Doc("Max count of commands awaiting to be flushed to the socket"), ] no_echo: Annotated[ bool, Doc("Boolean indicating should commands be echoed."), ] - tls: Annotated[ - Optional["ssl.SSLContext"], - Doc("Some SSL context to make NATS connections secure."), - ] tls_hostname: Annotated[ - Optional[str], + str | None, Doc("Hostname for TLS."), ] - user: Annotated[ - Optional[str], - Doc("Username for NATS auth."), - ] - password: Annotated[ - Optional[str], - Doc("Username password for NATS auth."), - ] token: Annotated[ - Optional[str], + str | None, Doc("Auth token for NATS auth."), ] drain_timeout: Annotated[ @@ -176,36 +165,36 @@ class NatsInitKwargs(TypedDict, total=False): Doc("Timeout in seconds to drain subscriptions."), ] signature_cb: Annotated[ - Optional["SignatureCallback"], + "SignatureCallback" | None, Doc( "A callback used to sign a nonce from the server while " "authenticating with nkeys. The user should sign the nonce and " - "return the base64 encoded signature." + "return the base64 encoded signature.", ), ] user_jwt_cb: Annotated[ - Optional["JWTCallback"], + "JWTCallback" | None, Doc( "A callback used to fetch and return the account " - "signed JWT for this user." + "signed JWT for this user.", ), ] user_credentials: Annotated[ - Optional["Credentials"], + "Credentials" | None, Doc("A user credentials file or tuple of files."), ] nkeys_seed: Annotated[ - Optional[str], + str | None, Doc("Path-like object containing nkeys seed that will be used."), ] nkeys_seed_str: Annotated[ - Optional[str], + str | None, Doc("Nkeys seed to be used."), ] inbox_prefix: Annotated[ - Union[str, bytes], + str | bytes, Doc( - "Prefix for generating unique inboxes, subjects with that prefix and NUID.ß" + "Prefix for generating unique inboxes, subjects with that prefix and NUID.ß", ), ] pending_size: Annotated[ @@ -213,29 +202,23 @@ class NatsInitKwargs(TypedDict, total=False): Doc("Max size of the pending buffer for publishing commands."), ] flush_timeout: Annotated[ - Optional[float], + float | None, Doc("Max duration to wait for a forced flush to occur."), ] class NatsBroker( NatsRegistrator, - NatsLoggingBroker, + BrokerUsecase[Msg, Client], ): """A class to represent a NATS broker.""" - url: List[str] - stream: Optional["JetStreamContext"] - - _producer: Optional["NatsFastProducer"] - _js_producer: Optional["NatsJSFastProducer"] - _kv_declarer: Optional["KVBucketDeclarer"] - _os_declarer: Optional["OSBucketDeclarer"] + url: list[str] def __init__( self, servers: Annotated[ - Union[str, Iterable[str]], + str | Iterable[str], Doc("NATS cluster addresses to connect."), ] = ("nats://localhost:4222",), *, @@ -256,17 +239,18 @@ def __init__( Doc("Callback to report when a new server joins the cluster."), ] = None, reconnected_cb: Annotated[ - Optional["Callback"], Doc("Callback to report success reconnection.") + Optional["Callback"], + Doc("Callback to report success reconnection."), ] = None, name: Annotated[ - Optional[str], + str | None, Doc("Label the connection with name (shown in NATS monitoring)."), ] = SERVICE_NAME, pedantic: Annotated[ bool, Doc( "Turn on NATS server pedantic mode that performs extra checks on the protocol. " - "https://docs.nats.io/using-nats/developer/connecting/misc#turn-on-pedantic-mode" + "https://docs.nats.io/using-nats/developer/connecting/misc#turn-on-pedantic-mode", ), ] = False, verbose: Annotated[ @@ -300,34 +284,23 @@ def __init__( dont_randomize: Annotated[ bool, Doc( - "Boolean indicating should client randomly shuffle servers list for reconnection randomness." + "Boolean indicating should client randomly shuffle servers list for reconnection randomness.", ), ] = False, flusher_queue_size: Annotated[ - int, Doc("Max count of commands awaiting to be flushed to the socket") + int, + Doc("Max count of commands awaiting to be flushed to the socket"), ] = DEFAULT_MAX_FLUSHER_QUEUE_SIZE, no_echo: Annotated[ bool, Doc("Boolean indicating should commands be echoed."), ] = False, - tls: Annotated[ - Optional["ssl.SSLContext"], - Doc("Some SSL context to make NATS connections secure."), - ] = None, tls_hostname: Annotated[ - Optional[str], + str | None, Doc("Hostname for TLS."), ] = None, - user: Annotated[ - Optional[str], - Doc("Username for NATS auth."), - ] = None, - password: Annotated[ - Optional[str], - Doc("Username password for NATS auth."), - ] = None, token: Annotated[ - Optional[str], + str | None, Doc("Auth token for NATS auth."), ] = None, drain_timeout: Annotated[ @@ -339,14 +312,14 @@ def __init__( Doc( "A callback used to sign a nonce from the server while " "authenticating with nkeys. The user should sign the nonce and " - "return the base64 encoded signature." + "return the base64 encoded signature.", ), ] = None, user_jwt_cb: Annotated[ Optional["JWTCallback"], Doc( "A callback used to fetch and return the account " - "signed JWT for this user." + "signed JWT for this user.", ), ] = None, user_credentials: Annotated[ @@ -354,17 +327,17 @@ def __init__( Doc("A user credentials file or tuple of files."), ] = None, nkeys_seed: Annotated[ - Optional[str], + str | None, Doc("Path-like object containing nkeys seed that will be used."), ] = None, nkeys_seed_str: Annotated[ - Optional[str], + str | None, Doc("Raw nkeys seed to be used."), ] = None, inbox_prefix: Annotated[ - Union[str, bytes], + str | bytes, Doc( - "Prefix for generating unique inboxes, subjects with that prefix and NUID.ß" + "Prefix for generating unique inboxes, subjects with that prefix and NUID.ß", ), ] = DEFAULT_INBOX_PREFIX, pending_size: Annotated[ @@ -372,14 +345,14 @@ def __init__( Doc("Max size of the pending buffer for publishing commands."), ] = DEFAULT_PENDING_SIZE, flush_timeout: Annotated[ - Optional[float], + float | None, Doc("Max duration to wait for a forced flush to occur."), ] = None, # broker args graceful_timeout: Annotated[ - Optional[float], + float | None, Doc( - "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down." + "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down.", ), ] = None, decoder: Annotated[ @@ -391,40 +364,44 @@ def __init__( Doc("Custom parser object."), ] = None, dependencies: Annotated[ - Iterable["Depends"], + Iterable["Dependant"], Doc("Dependencies to apply to all broker subscribers."), ] = (), middlewares: Annotated[ Sequence["BrokerMiddleware[Msg]"], Doc("Middlewares to apply to all broker publishers/subscribers."), ] = (), + routers: Annotated[ + Sequence["Registrator[Msg]"], + Doc("Routers to apply to broker."), + ] = (), # AsyncAPI args security: Annotated[ Optional["BaseSecurity"], Doc( - "Security options to connect broker and generate AsyncAPI server security information." + "Security options to connect broker and generate AsyncAPI server security information.", ), ] = None, - asyncapi_url: Annotated[ - Union[str, Iterable[str], None], + specification_url: Annotated[ + str | Iterable[str] | None, Doc("AsyncAPI hardcoded server addresses. Use `servers` if not specified."), ] = None, protocol: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server protocol."), ] = "nats", protocol_version: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server protocol version."), ] = "custom", description: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server description."), ] = None, tags: Annotated[ - Optional[Iterable[Union["asyncapi.Tag", "asyncapi.TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], @@ -434,69 +411,35 @@ def __init__( int, Doc("Service messages log level."), ] = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - Doc("Default logger log format."), - ] = EMPTY, # FastDepends args apply_types: Annotated[ bool, Doc("Whether to use FastDepends or not."), ] = True, - validate: Annotated[ - bool, - Doc("Whether to cast types using Pydantic validation."), - ] = True, - _get_dependant: Annotated[ - Optional[Callable[..., Any]], - Doc("Custom library dependant generator callback."), - ] = None, - _call_decorators: Annotated[ - Iterable["Decorator"], - Doc("Any custom decorator to apply to wrapped functions."), - ] = (), + serializer: Optional["SerializerProto"] = EMPTY, ) -> None: """Initialize the NatsBroker object.""" - if tls: # pragma: no cover - warnings.warn( - ( - "\nNATS `tls` option was deprecated and will be removed in 0.6.0" - "\nPlease, use `security` with `BaseSecurity` or `SASLPlaintext` instead" - ), - DeprecationWarning, - stacklevel=2, - ) - - if user or password: - warnings.warn( - ( - "\nNATS `user` and `password` options were deprecated and will be removed in 0.6.0" - "\nPlease, use `security` with `SASLPlaintext` instead" - ), - DeprecationWarning, - stacklevel=2, - ) - - secure_kwargs = { - "tls": tls, - "user": user, - "password": password, - **parse_security(security), - } + secure_kwargs = parse_security(security) servers = [servers] if isinstance(servers, str) else list(servers) - if asyncapi_url is not None: - if isinstance(asyncapi_url, str): - asyncapi_url = [asyncapi_url] + if specification_url is not None: + if isinstance(specification_url, str): + specification_url = [specification_url] else: - asyncapi_url = list(asyncapi_url) + specification_url = list(specification_url) else: - asyncapi_url = servers + specification_url = servers + + js_producer = NatsJSFastProducer( + parser=parser, + decoder=decoder, + ) + + producer = NatsFastProducerImpl( + parser=parser, + decoder=decoder, + ) super().__init__( # NATS options @@ -533,404 +476,268 @@ def __init__( signature_cb=signature_cb, user_jwt_cb=user_jwt_cb, # Basic args - # broker base - graceful_timeout=graceful_timeout, - dependencies=dependencies, - decoder=decoder, - parser=parser, - middlewares=middlewares, - # AsyncAPI - description=description, - asyncapi_url=asyncapi_url, - protocol=protocol, - protocol_version=protocol_version, - security=security, - tags=tags, - # logging - logger=logger, - log_level=log_level, - log_fmt=log_fmt, - # FastDepends args - apply_types=apply_types, - validate=validate, - _get_dependant=_get_dependant, - _call_decorators=_call_decorators, - ) - - self.__is_connected = False - self._producer = None - - # JS options - self.stream = None - self._js_producer = None - self._kv_declarer = None - self._os_declarer = None - - @override - async def connect( # type: ignore[override] - self, - servers: Annotated[ - Union[str, Iterable[str]], - Doc("NATS cluster addresses to connect."), - ] = EMPTY, - **kwargs: "Unpack[NatsInitKwargs]", - ) -> "Client": - """Connect broker object to NATS cluster. - - To startup subscribers too you should use `broker.start()` after/instead this method. - """ - if servers is not EMPTY or kwargs: - warnings.warn( - "`NatsBroker().connect(...) options were " - "deprecated in **FastStream 0.5.40**. " - "Please, use `NatsBroker(...)` instead. " - "All these options will be removed in **FastStream 0.6.0**.", - DeprecationWarning, - stacklevel=2, - ) - - if servers is not EMPTY: - connect_kwargs: AnyDict = { - **kwargs, - "servers": servers, - } - else: - connect_kwargs = {**kwargs} - - return await super().connect(**connect_kwargs) - - async def _connect(self, **kwargs: Any) -> "Client": - self.__is_connected = True - connection = await nats.connect(**kwargs) - - self._producer = NatsFastProducer( - connection=connection, - decoder=self._decoder, - parser=self._parser, - ) - - stream = self.stream = connection.jetstream() - - self._js_producer = NatsJSFastProducer( - connection=stream, - decoder=self._decoder, - parser=self._parser, + routers=routers, + config=NatsBrokerConfig( + producer=producer, + js_producer=js_producer, + # both args + broker_middlewares=middlewares, + broker_parser=parser, + broker_decoder=decoder, + logger=make_nats_logger_state( + logger=logger, + log_level=log_level, + ), + fd_config=FastDependsConfig( + use_fastdepends=apply_types, + serializer=serializer, + ), + # subscriber args + broker_dependencies=dependencies, + graceful_timeout=graceful_timeout, + extra_context={ + "broker": self, + }, + ), + specification=BrokerSpec( + description=description, + url=specification_url, + protocol=protocol, + protocol_version=protocol_version, + security=security, + tags=tags, + ), ) - self._kv_declarer = KVBucketDeclarer(stream) - self._os_declarer = OSBucketDeclarer(stream) - + async def _connect(self) -> "Client": + connection = await nats.connect(**self._connection_kwargs) + self.config.connect(connection) return connection - async def _close( + async def close( self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, exc_tb: Optional["TracebackType"] = None, ) -> None: - self._producer = None - self._js_producer = None - self.stream = None + await super().close(exc_type, exc_val, exc_tb) if self._connection is not None: await self._connection.drain() + self._connection = None - await super()._close(exc_type, exc_val, exc_tb) - self.__is_connected = False + self.config.disconnect() async def start(self) -> None: """Connect broker to NATS cluster and startup all subscribers.""" - await super().start() + await self.connect() - assert self._connection # nosec B101 - assert self.stream, "Broker should be started already" # nosec B101 - assert self._producer, "Broker should be started already" # nosec B101 + stream_context = self.config.connection_state.stream for stream in filter( lambda x: x.declare, self._stream_builder.objects.values(), ): try: - await self.stream.add_stream( + await stream_context.add_stream( config=stream.config, subjects=stream.subjects, ) except BadRequestError as e: # noqa: PERF203 - log_context = AsyncAPISubscriber.build_log_context( + self._setup_logger() + + log_context = LogicSubscriber.build_log_context( message=None, subject="", queue="", stream=stream.name, ) + logger_state = self.config.logger + if ( e.description == "stream name already in use with a different configuration" ): - old_config = (await self.stream.stream_info(stream.name)).config - - self._log(str(e), logging.WARNING, log_context) - await self.stream.update_stream( - config=stream.config, - subjects=tuple( - set(old_config.subjects or ()).union(stream.subjects) - ), - ) + old_config = (await stream_context.stream_info(stream.name)).config + + logger_state.log(str(e), logging.WARNING, log_context) + + for subject in old_config.subjects or (): + stream.add_subject(subject) + + await stream_context.update_stream(config=stream.config) else: # pragma: no cover - self._log(str(e), logging.ERROR, log_context, exc_info=e) + logger_state.log( + str(e), + logging.ERROR, + log_context, + exc_info=e, + ) finally: # prevent from double declaration stream.declare = False - # TODO: filter by already running handlers after TestClient refactor - for handler in self._subscribers.values(): - self._log( - f"`{handler.call_name}` waiting for messages", - extra=handler.get_log_context(None), - ) - await handler.start() + await super().start() + + @overload + async def publish( + self, + message: "SendableMessage", + subject: str, + headers: dict[str, str] | None = None, + reply_to: str = "", + correlation_id: str | None = None, + stream: None = None, + timeout: float | None = None, + ) -> None: ... + + @overload + async def publish( + self, + message: "SendableMessage", + subject: str, + headers: dict[str, str] | None = None, + reply_to: str = "", + correlation_id: str | None = None, + stream: str | None = None, + timeout: float | None = None, + ) -> "PubAck": ... @override - async def publish( # type: ignore[override] + async def publish( self, - message: Annotated[ - "SendableMessage", - Doc( - "Message body to send. " - "Can be any encodable object (native python types or `pydantic.BaseModel`)." - ), - ], - subject: Annotated[ - str, - Doc("NATS subject to send message."), - ], - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("NATS subject name to send response."), - ] = "", - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - stream: Annotated[ - Optional[str], - Doc( - "This option validates that the target subject is in presented stream. " - "Can be omitted without any effect." - ), - ] = None, - timeout: Annotated[ - Optional[float], - Doc("Timeout to send message to NATS."), - ] = None, - *, - rpc: Annotated[ - bool, - Doc("Whether to wait for reply in blocking mode."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - rpc_timeout: Annotated[ - Optional[float], - Doc("RPC reply waiting time."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method with `timeout` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = 30.0, - raise_timeout: Annotated[ - bool, - Doc( - "Whetever to raise `TimeoutError` or return `None` at **rpc_timeout**. " - "RPC request returns `None` at timeout by default." - ), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "`request` always raises TimeoutError instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - ) -> Optional["DecodedMessage"]: + message: "SendableMessage", + subject: str, + headers: dict[str, str] | None = None, + reply_to: str = "", + correlation_id: str | None = None, + stream: str | None = None, + timeout: float | None = None, + ) -> Optional["PubAck"]: """Publish message directly. This method allows you to publish message in not AsyncAPI-documented way. You can use it in another frameworks applications or to publish messages from time to time. Please, use `@broker.publisher(...)` or `broker.publisher(...).publish(...)` instead in a regular way. + + Args: + message: + Message body to send. + Can be any encodable object (native python types or `pydantic.BaseModel`). + subject: + NATS subject to send message. + headers: + Message headers to store metainformation. + **content-type** and **correlation_id** will be set automatically by framework anyway. + reply_to: + NATS subject name to send response. + correlation_id: + Manual message **correlation_id** setter. + **correlation_id** is a useful option to trace messages. + stream: + This option validates that the target subject is in presented stream. + Can be omitted without any effect if you doesn't want PubAck frame. + timeout: + Timeout to send message to NATS. + + Returns: + `None` if you publishes a regular message. + `faststream.nats.PubAck` if you publishes a message to stream. """ - publish_kwargs = { - "subject": subject, - "headers": headers, - "reply_to": reply_to, - "rpc": rpc, - "rpc_timeout": rpc_timeout, - "raise_timeout": raise_timeout, - } - - producer: Optional[ProducerProto] - if stream is None: - producer = self._producer - else: - producer = self._js_producer - publish_kwargs.update( - { - "stream": stream, - "timeout": timeout, - } - ) - - return await super().publish( - message, - producer=producer, + cmd = NatsPublishCommand( + message=message, correlation_id=correlation_id or gen_cor_id(), - **publish_kwargs, + subject=subject, + headers=headers, + reply_to=reply_to, + stream=stream, + timeout=timeout, + _publish_type=PublishType.PUBLISH, + ) + + producer = ( + self.config.js_producer if stream is not None else self.config.producer ) + return await super()._basic_publish(cmd, producer=producer) + @override async def request( # type: ignore[override] self, - message: Annotated[ - "SendableMessage", - Doc( - "Message body to send. " - "Can be any encodable object (native python types or `pydantic.BaseModel`)." - ), - ], - subject: Annotated[ - str, - Doc("NATS subject to send message."), - ], - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway." - ), - ] = None, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - stream: Annotated[ - Optional[str], - Doc( - "This option validates that the target subject is in presented stream. " - "Can be omitted without any effect." - ), - ] = None, - timeout: Annotated[ - float, - Doc("Timeout to send message to NATS."), - ] = 0.5, + message: "SendableMessage", + subject: str, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + stream: str | None = None, + timeout: float = 0.5, ) -> "NatsMessage": - publish_kwargs = { - "subject": subject, - "headers": headers, - "timeout": timeout, - } - - producer: Optional[ProducerProto] - if stream is None: - producer = self._producer - - else: - producer = self._js_producer - publish_kwargs.update({"stream": stream}) - - msg: NatsMessage = await super().request( - message, - producer=producer, + """Make a synchronous request to outer subscriber. + + If out subscriber listens subject by stream, you should setup the same **stream** explicitly. + Another way you will reseave confirmation frame as a response. + + Args: + message: + Message body to send. + Can be any encodable object (native python types or `pydantic.BaseModel`). + subject: + NATS subject to send message. + headers: + Message headers to store metainformation. + **content-type** and **correlation_id** will be set automatically by framework anyway. + reply_to: + NATS subject name to send response. + correlation_id: + Manual message **correlation_id** setter. + **correlation_id** is a useful option to trace messages. + stream: + JetStream name. This option is required if your target subscriber listens for events using JetStream. + timeout: + Timeout to send message to NATS. + + Returns: + `faststream.nats.message.NatsMessage` object as an outer subscriber response. + """ + cmd = NatsPublishCommand( + message=message, correlation_id=correlation_id or gen_cor_id(), - **publish_kwargs, + subject=subject, + headers=headers, + timeout=timeout, + stream=stream, + _publish_type=PublishType.REQUEST, ) - return msg - @override - def setup_subscriber( # type: ignore[override] - self, - subscriber: "AsyncAPISubscriber", - ) -> None: - connection: Union[ - Client, - JetStreamContext, - KVBucketDeclarer, - OSBucketDeclarer, - None, - ] = None - - if getattr(subscriber, "kv_watch", None): - connection = self._kv_declarer - - elif getattr(subscriber, "obj_watch", None): - connection = self._os_declarer - - elif getattr(subscriber, "stream", None): - connection = self.stream - - else: - connection = self._connection - - return super().setup_subscriber( - subscriber, - connection=connection, + producer = ( + self.config.js_producer if stream is not None else self.config.producer ) - @override - def setup_publisher( # type: ignore[override] - self, - publisher: "AsyncAPIPublisher", - ) -> None: - producer: Optional[ProducerProto] = None - - if publisher.stream is not None: - if self._js_producer is not None: - producer = self._js_producer - - elif self._producer is not None: - producer = self._producer - - super().setup_publisher(publisher, producer=producer) + msg: NatsMessage = await super()._basic_request(cmd, producer=producer) + return msg async def key_value( self, bucket: str, *, - description: Optional[str] = None, - max_value_size: Optional[int] = None, + description: str | None = None, + max_value_size: int | None = None, history: int = 1, - ttl: Optional[float] = None, # in seconds - max_bytes: Optional[int] = None, + ttl: float | None = None, # in seconds + max_bytes: int | None = None, storage: Optional["StorageType"] = None, replicas: int = 1, placement: Optional["Placement"] = None, republish: Optional["RePublish"] = None, - direct: Optional[bool] = None, + direct: bool | None = None, # custom declare: bool = True, ) -> "KeyValue": - assert self._kv_declarer, "Broker should be connected already." # nosec B101 - - return await self._kv_declarer.create_key_value( + return await self.config.kv_declarer.create_key_value( bucket=bucket, description=description, max_value_size=max_value_size, @@ -949,18 +756,16 @@ async def object_storage( self, bucket: str, *, - description: Optional[str] = None, - ttl: Optional[float] = None, - max_bytes: Optional[int] = None, + description: str | None = None, + ttl: float | None = None, + max_bytes: int | None = None, storage: Optional["StorageType"] = None, replicas: int = 1, placement: Optional["Placement"] = None, # custom declare: bool = True, ) -> "ObjectStore": - assert self._os_declarer, "Broker should be connected already." # nosec B101 - - return await self._os_declarer.create_object_store( + return await self.config.os_declarer.create_object_store( bucket=bucket, description=description, ttl=ttl, @@ -975,17 +780,19 @@ def _log_connection_broken( self, error_cb: Optional["ErrorCallback"] = None, ) -> "ErrorCallback": - c = AsyncAPISubscriber.build_log_context(None, "") + c = LogicSubscriber.build_log_context(None, "") async def wrapper(err: Exception) -> None: if error_cb is not None: await error_cb(err) - if isinstance(err, Error) and self.__is_connected: - self._log( - f"Connection broken with {err!r}", logging.WARNING, c, exc_info=err + if isinstance(err, Error) and self.config.connection_state: + self.config.logger.log( + f"Connection broken with {err!r}", + logging.WARNING, + c, + exc_info=err, ) - self.__is_connected = False return wrapper @@ -993,15 +800,14 @@ def _log_reconnected( self, cb: Optional["Callback"] = None, ) -> "Callback": - c = AsyncAPISubscriber.build_log_context(None, "") + c = LogicSubscriber.build_log_context(None, "") async def wrapper() -> None: if cb is not None: await cb() - if not self.__is_connected: - self._log("Connection established", logging.INFO, c) - self.__is_connected = True + if not self.config.connection_state: + self.config.logger.log("Connection established", logging.INFO, c) return wrapper @@ -1019,7 +825,7 @@ async def new_inbox(self) -> str: return self._connection.new_inbox() @override - async def ping(self, timeout: Optional[float]) -> bool: + async def ping(self, timeout: float | None) -> bool: sleep_time = (timeout or 10) / 10 with anyio.move_on_after(timeout) as cancel_scope: diff --git a/faststream/nats/broker/logging.py b/faststream/nats/broker/logging.py index fba1e3493d..fcae45b560 100644 --- a/faststream/nats/broker/logging.py +++ b/faststream/nats/broker/logging.py @@ -1,81 +1,89 @@ import logging -from typing import TYPE_CHECKING, Any, ClassVar, Optional +from functools import partial +from typing import TYPE_CHECKING -from nats.aio.client import Client -from nats.aio.msg import Msg -from typing_extensions import Annotated, deprecated - -from faststream.broker.core.usecase import BrokerUsecase -from faststream.log.logging import get_broker_logger -from faststream.types import EMPTY +from faststream._internal.logger import ( + DefaultLoggerStorage, + make_logger_state, +) +from faststream._internal.logger.logging import get_broker_logger if TYPE_CHECKING: - from faststream.types import LoggerProto + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.context import ContextRepo + + +class NatsParamsStorage(DefaultLoggerStorage): + def __init__(self) -> None: + super().__init__() + self._max_queue_len = 0 + self._max_stream_len = 0 + self._max_subject_len = 4 -class NatsLoggingBroker(BrokerUsecase[Msg, Client]): - """A class that extends the LoggingMixin class and adds additional functionality for logging NATS related information.""" + self.logger_log_level = logging.INFO - _max_queue_len: int - _max_subject_len: int - __max_msg_id_ln: ClassVar[int] = 10 + def set_level(self, level: int) -> None: + self.logger_log_level = level - def __init__( - self, - *args: Any, - logger: Optional["LoggerProto"] = EMPTY, - log_level: int = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." + def register_subscriber(self, params: "AnyDict") -> None: + self._max_subject_len = max( + ( + self._max_subject_len, + len(params.get("subject", "")), + ), + ) + self._max_queue_len = max( + ( + self._max_queue_len, + len(params.get("queue", "")), + ), + ) + self._max_stream_len = max( + ( + self._max_stream_len, + len(params.get("stream", "")), ), - ] = EMPTY, - **kwargs: Any, - ) -> None: - """Initialize the NATS logging mixin.""" - super().__init__( - *args, - logger=logger, - # TODO: generate unique logger names to not share between brokers - default_logger=get_broker_logger( + ) + + def get_logger(self, *, context: "ContextRepo") -> "LoggerProto": + # TODO: generate unique logger names to not share between brokers + if not (lg := self._get_logger_ref()): + message_id_ln = 10 + + lg = get_broker_logger( name="nats", default_context={ "subject": "", "stream": "", "queue": "", }, - message_id_ln=self.__max_msg_id_ln, - ), - log_level=log_level, - log_fmt=log_fmt, - **kwargs, - ) + message_id_ln=message_id_ln, + fmt="".join(( + "%(asctime)s %(levelname)-8s - ", + ( + f"%(stream)-{self._max_stream_len}s | " + if self._max_stream_len + else "" + ), + ( + f"%(queue)-{self._max_queue_len}s | " + if self._max_queue_len + else "" + ), + f"%(subject)-{self._max_subject_len}s | ", + f"%(message_id)-{message_id_ln}s - ", + "%(message)s", + )), + context=context, + log_level=self.logger_log_level, + ) + self._logger_ref.add(lg) - self._max_queue_len = 0 - self._max_stream_len = 0 - self._max_subject_len = 4 + return lg - def get_fmt(self) -> str: - """Fallback method to get log format if `log_fmt` if not specified.""" - return ( - "%(asctime)s %(levelname)-8s - " - + (f"%(stream)-{self._max_stream_len}s | " if self._max_stream_len else "") - + (f"%(queue)-{self._max_queue_len}s | " if self._max_queue_len else "") - + f"%(subject)-{self._max_subject_len}s | " - + f"%(message_id)-{self.__max_msg_id_ln}s - " - "%(message)s" - ) - def _setup_log_context( - self, - *, - queue: Optional[str] = None, - subject: Optional[str] = None, - stream: Optional[str] = None, - ) -> None: - """Setup subscriber's information to generate default log format.""" - self._max_subject_len = max((self._max_subject_len, len(subject or ""))) - self._max_queue_len = max((self._max_queue_len, len(queue or ""))) - self._max_stream_len = max((self._max_stream_len, len(stream or ""))) +make_nats_logger_state = partial( + make_logger_state, + default_storage_cls=NatsParamsStorage, +) diff --git a/faststream/nats/broker/registrator.py b/faststream/nats/broker/registrator.py index 23bd0f077f..c11696e28c 100644 --- a/faststream/nats/broker/registrator.py +++ b/faststream/nats/broker/registrator.py @@ -1,36 +1,37 @@ -from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Sequence, Union, cast +from collections.abc import Iterable, Sequence +from typing import TYPE_CHECKING, Annotated, Any, Optional, Union, cast from nats.js import api -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import Doc, deprecated, override -from faststream.broker.core.abc import ABCBroker -from faststream.broker.utils import default_filter +from faststream._internal.broker.abc_broker import Registrator +from faststream._internal.constants import EMPTY from faststream.exceptions import SetupError +from faststream.middlewares import AckPolicy from faststream.nats.helpers import StreamBuilder -from faststream.nats.publisher.asyncapi import AsyncAPIPublisher +from faststream.nats.publisher.factory import create_publisher from faststream.nats.schemas import JStream, KvWatch, ObjWatch, PullSub -from faststream.nats.subscriber.asyncapi import AsyncAPISubscriber from faststream.nats.subscriber.factory import create_subscriber if TYPE_CHECKING: - from fast_depends.dependencies import Depends + from fast_depends.dependencies import Dependant from nats.aio.msg import Msg - from faststream.broker.types import ( + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, - Filter, PublisherMiddleware, SubscriberMiddleware, ) - from faststream.nats.message import NatsBatchMessage, NatsMessage + from faststream.nats.configs import NatsBrokerConfig + from faststream.nats.message import NatsMessage + from faststream.nats.publisher.usecase import LogicPublisher -class NatsRegistrator(ABCBroker["Msg"]): +class NatsRegistrator(Registrator["Msg"]): """Includable to NatsBroker router.""" - _subscribers: Dict[int, "AsyncAPISubscriber"] - _publishers: Dict[int, "AsyncAPIPublisher"] + config: "NatsBrokerConfig" def __init__(self, **kwargs: Any) -> None: self._stream_builder = StreamBuilder() @@ -48,27 +49,27 @@ def subscriber( # type: ignore[override] str, Doc( "Subscribers' NATS queue name. Subscribers with same queue name will be load balanced by the NATS " - "server." + "server.", ), ] = "", pending_msgs_limit: Annotated[ - Optional[int], + int | None, Doc( "Limit of messages, considered by NATS server as possible to be delivered to the client without " "been answered. In case of NATS Core, if that limits exceeds, you will receive NATS 'Slow Consumer' " "error. " "That's literally means that your worker can't handle the whole load. In case of NATS JetStream, " - "you will no longer receive messages until some of delivered messages will be acked in any way." + "you will no longer receive messages until some of delivered messages will be acked in any way.", ), ] = None, pending_bytes_limit: Annotated[ - Optional[int], + int | None, Doc( "The number of bytes, considered by NATS server as possible to be delivered to the client without " "been answered. In case of NATS Core, if that limit exceeds, you will receive NATS 'Slow Consumer' " "error." "That's literally means that your worker can't handle the whole load. In case of NATS JetStream, " - "you will no longer receive messages until some of delivered messages will be acked in any way." + "you will no longer receive messages until some of delivered messages will be acked in any way.", ), ] = None, # Core arguments @@ -78,9 +79,9 @@ def subscriber( # type: ignore[override] ] = 0, # JS arguments durable: Annotated[ - Optional[str], + str | None, Doc( - "Name of the durable consumer to which the the subscription should be bound." + "Name of the durable consumer to which the the subscription should be bound.", ), ] = None, config: Annotated[ @@ -92,11 +93,11 @@ def subscriber( # type: ignore[override] Doc("Enable ordered consumer mode."), ] = False, idle_heartbeat: Annotated[ - Optional[float], + float | None, Doc("Enable Heartbeats for a consumer to detect failures."), ] = None, flow_control: Annotated[ - Optional[bool], + bool | None, Doc("Enable Flow Control for a consumer."), ] = None, deliver_policy: Annotated[ @@ -104,9 +105,9 @@ def subscriber( # type: ignore[override] Doc("Deliver Policy to be used for subscription."), ] = None, headers_only: Annotated[ - Optional[bool], + bool | None, Doc( - "Should be message delivered without payload, only headers and metadata." + "Should be message delivered without payload, only headers and metadata.", ), ] = None, # pull arguments @@ -114,7 +115,7 @@ def subscriber( # type: ignore[override] Union[bool, "PullSub"], Doc( "NATS Pull consumer parameters container. " - "Should be used with `stream` only." + "Should be used with `stream` only.", ), ] = False, kv_watch: Annotated[ @@ -128,22 +129,28 @@ def subscriber( # type: ignore[override] inbox_prefix: Annotated[ bytes, Doc( - "Prefix for generating unique inboxes, subjects with that prefix and NUID." + "Prefix for generating unique inboxes, subjects with that prefix and NUID.", ), ] = api.INBOX_PREFIX, # custom ack_first: Annotated[ bool, Doc("Whether to `ack` message at start of consuming or not."), - ] = False, + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, stream: Annotated[ Union[str, "JStream", None], Doc("Subscribe to NATS Stream with `subject` filter."), ] = None, # broker arguments dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), + Iterable["Dependant"], + Doc("Dependencies list (`[Dependant(),]`) to apply to the subscriber."), ] = (), parser: Annotated[ Optional["CustomCallable"], @@ -155,71 +162,55 @@ def subscriber( # type: ignore[override] ] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[NatsMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - Union[ - "Filter[NatsMessage]", - "Filter[NatsBatchMessage]", - ], - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = default_filter, + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), max_workers: Annotated[ int, Doc("Number of workers to process messages concurrently."), ] = 1, - retry: Annotated[ + no_ack: Annotated[ bool, - Doc("Whether to `nack` message at processing exception."), + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), deprecated( - "Deprecated in **FastStream 0.5.40**." - "Please, manage acknowledgement policy manually." - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" ), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, # AsyncAPI information title: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ bool, Doc("Whetever to include operation in AsyncAPI schema or not."), ] = True, - ) -> AsyncAPISubscriber: + ): """Creates NATS subscriber object. You can use it as a handler decorator `@broker.subscriber(...)`. """ stream = self._stream_builder.create(stream) - subscriber = cast( - "AsyncAPISubscriber", - super().subscriber( + subscriber = super().subscriber( create_subscriber( subject=subject, queue=queue, @@ -242,26 +233,23 @@ def subscriber( # type: ignore[override] inbox_prefix=inbox_prefix, ack_first=ack_first, # subscriber args + ack_policy=ack_policy, no_ack=no_ack, no_reply=no_reply, - retry=retry, - broker_middlewares=self._middlewares, - broker_dependencies=self._dependencies, + broker_config=self.config, # AsyncAPI title_=title, description_=description, - include_in_schema=self._solve_include_in_schema(include_in_schema), - ) - ), - ) + include_in_schema=include_in_schema, + ), + ) if stream and subscriber.subject: stream.add_subject(subscriber.subject) return subscriber.add_call( - filter_=filter, - parser_=parser or self._parser, - decoder_=decoder or self._decoder, + parser_=parser, + decoder_=decoder, dependencies_=dependencies, middlewares_=middlewares, ) @@ -275,11 +263,11 @@ def publisher( # type: ignore[override] ], *, headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc( "Message headers to store metainformation. " "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -291,39 +279,43 @@ def publisher( # type: ignore[override] Union[str, "JStream", None], Doc( "This option validates that the target `subject` is in presented stream. " - "Can be omitted without any effect." + "Can be omitted without any effect.", ), ] = None, timeout: Annotated[ - Optional[float], + float | None, Doc("Timeout to send message to NATS."), ] = None, # basic args middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), # AsyncAPI information title: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI publisher object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, Doc("Whetever to include operation in AsyncAPI schema or not."), ] = True, - ) -> "AsyncAPIPublisher": + ) -> "LogicPublisher": """Creates long-living and AsyncAPI-documented publisher object. You can use it as a handler decorator (handler should be decorated by `@broker.subscriber(...)` too) - `@broker.publisher(...)`. @@ -334,9 +326,9 @@ def publisher( # type: ignore[override] stream = self._stream_builder.create(stream) publisher = cast( - "AsyncAPIPublisher", + "LogicPublisher", super().publisher( - publisher=AsyncAPIPublisher.create( + publisher=create_publisher( subject=subject, headers=headers, # Core @@ -345,14 +337,14 @@ def publisher( # type: ignore[override] timeout=timeout, stream=stream, # Specific - broker_middlewares=self._middlewares, + broker_config=self.config, middlewares=middlewares, # AsyncAPI title_=title, description_=description, schema_=schema, - include_in_schema=self._solve_include_in_schema(include_in_schema), - ) + include_in_schema=include_in_schema, + ), ), ) @@ -367,9 +359,9 @@ def include_router( router: "NatsRegistrator", # type: ignore[override] *, prefix: str = "", - dependencies: Iterable["Depends"] = (), - middlewares: Iterable["BrokerMiddleware[Msg]"] = (), - include_in_schema: Optional[bool] = None, + dependencies: Iterable["Dependant"] = (), + middlewares: Sequence["BrokerMiddleware[Msg]"] = (), + include_in_schema: bool | None = None, ) -> None: if not isinstance(router, NatsRegistrator): msg = ( @@ -380,13 +372,13 @@ def include_router( sub_streams = router._stream_builder.objects.copy() - sub_router_subjects = [sub.subject for sub in router._subscribers.values()] + sub_router_subjects = [sub.subject for sub in router.subscribers] for stream in sub_streams.values(): new_subjects = [] for subj in stream.subjects: if subj in sub_router_subjects: - new_subjects.append("".join((self.prefix, subj))) + new_subjects.append(f"{self.config.prefix}{subj}") else: new_subjects.append(subj) stream.subjects = new_subjects diff --git a/faststream/nats/broker/router.py b/faststream/nats/broker/router.py new file mode 100644 index 0000000000..11e58675aa --- /dev/null +++ b/faststream/nats/broker/router.py @@ -0,0 +1,390 @@ +from collections.abc import Awaitable, Callable, Iterable, Sequence +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Optional, + Union, +) + +from nats.js import api +from typing_extensions import Doc, deprecated + +from faststream._internal.broker.router import ( + ArgsContainer, + BrokerRouter, + SubscriberRoute, +) +from faststream._internal.constants import EMPTY +from faststream.middlewares import AckPolicy +from faststream.nats.configs import NatsBrokerConfig + +from .registrator import NatsRegistrator + +if TYPE_CHECKING: + from fast_depends.dependencies import Dependant + from nats.aio.msg import Msg + + from faststream._internal.basic_types import SendableMessage + from faststream._internal.broker.abc_broker import Registrator + from faststream._internal.types import ( + BrokerMiddleware, + CustomCallable, + PublisherMiddleware, + SubscriberMiddleware, + ) + from faststream.nats.message import NatsMessage + from faststream.nats.schemas import JStream, KvWatch, ObjWatch, PullSub + + +class NatsPublisher(ArgsContainer): + """Delayed NatsPublisher registration object. + + Just a copy of `KafkaRegistrator.publisher(...)` arguments. + """ + + def __init__( + self, + subject: Annotated[ + str, + Doc("NATS subject to send message."), + ], + *, + headers: Annotated[ + dict[str, str] | None, + Doc( + "Message headers to store metainformation. " + "**content-type** and **correlation_id** will be set automatically by framework anyway. " + "Can be overridden by `publish.headers` if specified.", + ), + ] = None, + reply_to: Annotated[ + str, + Doc("NATS subject name to send response."), + ] = "", + # JS + stream: Annotated[ + Union[str, "JStream", None], + Doc( + "This option validates that the target `subject` is in presented stream. " + "Can be omitted without any effect.", + ), + ] = None, + timeout: Annotated[ + float | None, + Doc("Timeout to send message to NATS."), + ] = None, + # basic args + middlewares: Annotated[ + Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + Doc("Publisher middlewares to wrap outgoing messages."), + ] = (), + # AsyncAPI information + title: Annotated[ + str | None, + Doc("AsyncAPI publisher object title."), + ] = None, + description: Annotated[ + str | None, + Doc("AsyncAPI publisher object description."), + ] = None, + schema: Annotated[ + Any | None, + Doc( + "AsyncAPI publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = True, + ) -> None: + super().__init__( + subject=subject, + headers=headers, + reply_to=reply_to, + stream=stream, + timeout=timeout, + middlewares=middlewares, + title=title, + description=description, + schema=schema, + include_in_schema=include_in_schema, + ) + + +class NatsRoute(SubscriberRoute): + """Class to store delayed NatsBroker subscriber registration.""" + + def __init__( + self, + call: Annotated[ + Callable[..., "SendableMessage"] | Callable[..., Awaitable["SendableMessage"]], + Doc( + "Message handler function " + "to wrap the same with `@broker.subscriber(...)` way.", + ), + ], + subject: Annotated[ + str, + Doc("NATS subject to subscribe."), + ], + publishers: Annotated[ + Iterable[NatsPublisher], + Doc("Nats publishers to broadcast the handler result."), + ] = (), + queue: Annotated[ + str, + Doc( + "Subscribers' NATS queue name. Subscribers with same queue name will be load balanced by the NATS " + "server.", + ), + ] = "", + pending_msgs_limit: Annotated[ + int | None, + Doc( + "Limit of messages, considered by NATS server as possible to be delivered to the client without " + "been answered. In case of NATS Core, if that limits exceeds, you will receive NATS 'Slow Consumer' " + "error. " + "That's literally means that your worker can't handle the whole load. In case of NATS JetStream, " + "you will no longer receive messages until some of delivered messages will be acked in any way.", + ), + ] = None, + pending_bytes_limit: Annotated[ + int | None, + Doc( + "The number of bytes, considered by NATS server as possible to be delivered to the client without " + "been answered. In case of NATS Core, if that limit exceeds, you will receive NATS 'Slow Consumer' " + "error." + "That's literally means that your worker can't handle the whole load. In case of NATS JetStream, " + "you will no longer receive messages until some of delivered messages will be acked in any way.", + ), + ] = None, + # Core arguments + max_msgs: Annotated[ + int, + Doc("Consuming messages limiter. Automatically disconnect if reached."), + ] = 0, + # JS arguments + durable: Annotated[ + str | None, + Doc( + "Name of the durable consumer to which the the subscription should be bound.", + ), + ] = None, + config: Annotated[ + Optional["api.ConsumerConfig"], + Doc("Configuration of JetStream consumer to be subscribed with."), + ] = None, + ordered_consumer: Annotated[ + bool, + Doc("Enable ordered consumer mode."), + ] = False, + idle_heartbeat: Annotated[ + float | None, + Doc("Enable Heartbeats for a consumer to detect failures."), + ] = None, + flow_control: Annotated[ + bool | None, + Doc("Enable Flow Control for a consumer."), + ] = None, + deliver_policy: Annotated[ + Optional["api.DeliverPolicy"], + Doc("Deliver Policy to be used for subscription."), + ] = None, + headers_only: Annotated[ + bool | None, + Doc( + "Should be message delivered without payload, only headers and metadata.", + ), + ] = None, + # pull arguments + pull_sub: Annotated[ + Optional["PullSub"], + Doc( + "NATS Pull consumer parameters container. " + "Should be used with `stream` only.", + ), + ] = None, + kv_watch: Annotated[ + Union[str, "KvWatch", None], + Doc("KeyValue watch parameters container."), + ] = None, + obj_watch: Annotated[ + Union[bool, "ObjWatch"], + Doc("ObjecStore watch parameters container."), + ] = False, + inbox_prefix: Annotated[ + bytes, + Doc( + "Prefix for generating unique inboxes, subjects with that prefix and NUID.", + ), + ] = api.INBOX_PREFIX, + # custom + ack_first: Annotated[ + bool, + Doc("Whether to `ack` message at start of consuming or not."), + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, + stream: Annotated[ + Union[str, "JStream", None], + Doc("Subscribe to NATS Stream with `subject` filter."), + ] = None, + # broker arguments + dependencies: Annotated[ + Iterable["Dependant"], + Doc("Dependencies list (`[Dependant(),]`) to apply to the subscriber."), + ] = (), + parser: Annotated[ + Optional["CustomCallable"], + Doc("Parser to map original **nats-py** Msg to FastStream one."), + ] = None, + decoder: Annotated[ + Optional["CustomCallable"], + Doc("Function to decode FastStream msg bytes body to python objects."), + ] = None, + middlewares: Annotated[ + Sequence["SubscriberMiddleware[NatsMessage]"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), + max_workers: Annotated[ + int, + Doc("Number of workers to process messages concurrently."), + ] = 1, + no_ack: Annotated[ + bool, + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), + deprecated( + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" + ), + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: Annotated[ + bool, + Doc( + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", + ), + ] = False, + # AsyncAPI information + title: Annotated[ + str | None, + Doc("AsyncAPI subscriber object title."), + ] = None, + description: Annotated[ + str | None, + Doc( + "AsyncAPI subscriber object description. " + "Uses decorated docstring as default.", + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = True, + ) -> None: + super().__init__( + call, + subject=subject, + publishers=publishers, + pending_msgs_limit=pending_msgs_limit, + pending_bytes_limit=pending_bytes_limit, + max_msgs=max_msgs, + durable=durable, + config=config, + ordered_consumer=ordered_consumer, + idle_heartbeat=idle_heartbeat, + flow_control=flow_control, + deliver_policy=deliver_policy, + headers_only=headers_only, + pull_sub=pull_sub, + kv_watch=kv_watch, + obj_watch=obj_watch, + inbox_prefix=inbox_prefix, + ack_first=ack_first, + stream=stream, + max_workers=max_workers, + queue=queue, + dependencies=dependencies, + parser=parser, + decoder=decoder, + middlewares=middlewares, + ack_policy=ack_policy, + no_ack=no_ack, + no_reply=no_reply, + title=title, + description=description, + include_in_schema=include_in_schema, + ) + + +class NatsRouter( + NatsRegistrator, + BrokerRouter["Msg"], +): + """Includable to NatsBroker router.""" + + def __init__( + self, + prefix: Annotated[ + str, + Doc("String prefix to add to all subscribers subjects."), + ] = "", + handlers: Annotated[ + Iterable[NatsRoute], + Doc("Route object to include."), + ] = (), + *, + dependencies: Annotated[ + Iterable["Dependant"], + Doc( + "Dependencies list (`[Dependant(),]`) to apply to all routers' publishers/subscribers.", + ), + ] = (), + middlewares: Annotated[ + Sequence["BrokerMiddleware[Msg]"], + Doc("Router middlewares to apply to all routers' publishers/subscribers."), + ] = (), + routers: Annotated[ + Sequence["Registrator[Msg]"], + Doc("Routers to apply to broker."), + ] = (), + parser: Annotated[ + Optional["CustomCallable"], + Doc("Parser to map original **IncomingMessage** Msg to FastStream one."), + ] = None, + decoder: Annotated[ + Optional["CustomCallable"], + Doc("Function to decode FastStream msg bytes body to python objects."), + ] = None, + include_in_schema: Annotated[ + bool | None, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = None, + ) -> None: + super().__init__( + handlers=handlers, + config=NatsBrokerConfig( + broker_middlewares=middlewares, + broker_dependencies=dependencies, + broker_parser=parser, + broker_decoder=decoder, + include_in_schema=include_in_schema, + prefix=prefix, + ), + routers=routers, + ) diff --git a/faststream/nats/broker/state.py b/faststream/nats/broker/state.py new file mode 100644 index 0000000000..d8390c616d --- /dev/null +++ b/faststream/nats/broker/state.py @@ -0,0 +1,45 @@ +from typing import TYPE_CHECKING + +from faststream.exceptions import IncorrectState + +if TYPE_CHECKING: + from nats.aio.client import Client + from nats.js import JetStreamContext + + +class BrokerState: + stream: "JetStreamContext" + connection: "Client" + + def __init__(self) -> None: + self._connected = False + + self._stream: JetStreamContext | None = None + self._connection: Client | None = None + + @property + def connection(self) -> "Client": + if not self._connection: + msg = "Connection is not available yet. Please, connect the broker first." + raise IncorrectState(msg) + return self._connection + + @property + def stream(self) -> "JetStreamContext": + if not self._stream: + msg = "Stream is not available yet. Please, connect the broker first." + raise IncorrectState(msg) + return self._stream + + def __bool__(self) -> bool: + return self._connected + + def connect(self, connection: "Client", stream: "JetStreamContext") -> None: + self._connection = connection + self._stream = stream + self._connected = True + + def disconnect(self) -> None: + self._connection = None + self._stream = None + self._connected = False diff --git a/faststream/nats/configs/__init__.py b/faststream/nats/configs/__init__.py new file mode 100644 index 0000000000..f0c0255430 --- /dev/null +++ b/faststream/nats/configs/__init__.py @@ -0,0 +1,5 @@ +from .broker import NatsBrokerConfig + +__all__ = ( + "NatsBrokerConfig", +) diff --git a/faststream/nats/configs/broker.py b/faststream/nats/configs/broker.py new file mode 100644 index 0000000000..8a47a47320 --- /dev/null +++ b/faststream/nats/configs/broker.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from faststream._internal.configs import BrokerConfig +from faststream.nats.broker.state import BrokerState +from faststream.nats.helpers import KVBucketDeclarer, OSBucketDeclarer +from faststream.nats.publisher.producer import ( + FakeNatsFastProducer, + NatsFastProducer, + NatsJSFastProducer, +) + +if TYPE_CHECKING: + from nats.aio.client import Client + + +@dataclass(kw_only=True) +class NatsBrokerConfig(BrokerConfig): + producer: "NatsFastProducer" = field(default_factory=FakeNatsFastProducer) + js_producer: "NatsJSFastProducer" = field(default_factory=FakeNatsFastProducer) + connection_state: BrokerState = field(default_factory=BrokerState) + kv_declarer: KVBucketDeclarer = field(default_factory=KVBucketDeclarer) + os_declarer: OSBucketDeclarer = field(default_factory=OSBucketDeclarer) + + def connect(self, connection: "Client") -> None: + stream = connection.jetstream() + + self.producer.connect(connection, serializer=self.fd_config._serializer) + + self.js_producer.connect(stream, serializer=self.fd_config._serializer) + self.kv_declarer.connect(stream) + self.os_declarer.connect(stream) + + self.connection_state.connect(connection, stream) + + def disconnect(self) -> None: + self.producer.disconnect() + self.js_producer.disconnect() + self.kv_declarer.disconnect() + self.os_declarer.disconnect() + + self.connection_state.disconnect() diff --git a/faststream/nats/fastapi/__init__.py b/faststream/nats/fastapi/__init__.py index 56b2eb0f05..2c0acf3c3c 100644 --- a/faststream/nats/fastapi/__init__.py +++ b/faststream/nats/fastapi/__init__.py @@ -1,19 +1,18 @@ +from typing import Annotated + from nats.aio.client import Client as NatsClient from nats.js.client import JetStreamContext -from typing_extensions import Annotated -from faststream.broker.fastapi.context import Context, ContextRepo, Logger +from faststream._internal.fastapi.context import Context, ContextRepo, Logger from faststream.nats.broker import NatsBroker as NB -from faststream.nats.fastapi.fastapi import NatsRouter from faststream.nats.message import NatsMessage as NM -from faststream.nats.publisher.producer import NatsFastProducer, NatsJSFastProducer + +from .fastapi import NatsRouter NatsMessage = Annotated[NM, Context("message")] NatsBroker = Annotated[NB, Context("broker")] Client = Annotated[NatsClient, Context("broker._connection")] JsClient = Annotated[JetStreamContext, Context("broker._stream")] -NatsProducer = Annotated[NatsFastProducer, Context("broker._producer")] -NatsJsProducer = Annotated[NatsJSFastProducer, Context("broker._js_producer")] __all__ = ( "Client", @@ -22,8 +21,7 @@ "JsClient", "Logger", "NatsBroker", - "NatsJsProducer", "NatsMessage", - "NatsProducer", + "NatsMessage", "NatsRouter", ) diff --git a/faststream/nats/fastapi/fastapi.py b/faststream/nats/fastapi/fastapi.py index 00b9e926ff..3efcd476aa 100644 --- a/faststream/nats/fastapi/fastapi.py +++ b/faststream/nats/fastapi/fastapi.py @@ -1,14 +1,10 @@ import logging +from collections.abc import Callable, Iterable, Sequence from typing import ( TYPE_CHECKING, + Annotated, Any, - Callable, - Dict, - Iterable, - List, Optional, - Sequence, - Type, Union, cast, ) @@ -30,17 +26,15 @@ from nats.js import api from starlette.responses import JSONResponse from starlette.routing import BaseRoute -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import Doc, deprecated, override from faststream.__about__ import SERVICE_NAME -from faststream.broker.fastapi.router import StreamRouter -from faststream.broker.utils import default_filter +from faststream._internal.constants import EMPTY +from faststream._internal.fastapi.router import StreamRouter +from faststream.middlewares import AckPolicy from faststream.nats.broker import NatsBroker -from faststream.nats.publisher.asyncapi import AsyncAPIPublisher -from faststream.types import EMPTY if TYPE_CHECKING: - import ssl from enum import Enum from fastapi import params @@ -56,19 +50,19 @@ from starlette.responses import Response from starlette.types import ASGIApp, Lifespan - from faststream.asyncapi import schema as asyncapi - from faststream.broker.types import ( + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, - Filter, PublisherMiddleware, SubscriberMiddleware, ) - from faststream.nats.message import NatsBatchMessage, NatsMessage + from faststream.nats.message import NatsMessage + from faststream.nats.publisher.usecase import LogicPublisher from faststream.nats.schemas import JStream, KvWatch, ObjWatch, PullSub - from faststream.nats.subscriber.asyncapi import AsyncAPISubscriber + from faststream.nats.subscriber.specification import SpecificationSubscriber from faststream.security import BaseSecurity - from faststream.types import AnyDict, LoggerProto + from faststream.specification.schema.extra import Tag, TagDict class NatsRouter(StreamRouter["Msg"]): @@ -80,7 +74,7 @@ class NatsRouter(StreamRouter["Msg"]): def __init__( self, servers: Annotated[ - Union[str, Iterable[str]], + str | Iterable[str], Doc("NATS cluster addresses to connect."), ] = ("nats://localhost:4222",), *, @@ -102,17 +96,18 @@ def __init__( Doc("Callback to report when a new server joins the cluster."), ] = None, reconnected_cb: Annotated[ - Optional["Callback"], Doc("Callback to report success reconnection.") + Optional["Callback"], + Doc("Callback to report success reconnection."), ] = None, name: Annotated[ - Optional[str], + str | None, Doc("Label the connection with name (shown in NATS monitoring)."), ] = SERVICE_NAME, pedantic: Annotated[ bool, Doc( "Turn on NATS server pedantic mode that performs extra checks on the protocol. " - "https://docs.nats.io/using-nats/developer/connecting/misc#turn-on-pedantic-mode" + "https://docs.nats.io/using-nats/developer/connecting/misc#turn-on-pedantic-mode", ), ] = False, verbose: Annotated[ @@ -146,34 +141,23 @@ def __init__( dont_randomize: Annotated[ bool, Doc( - "Boolean indicating should client randomly shuffle servers list for reconnection randomness." + "Boolean indicating should client randomly shuffle servers list for reconnection randomness.", ), ] = False, flusher_queue_size: Annotated[ - int, Doc("Max count of commands awaiting to be flushed to the socket") + int, + Doc("Max count of commands awaiting to be flushed to the socket"), ] = DEFAULT_MAX_FLUSHER_QUEUE_SIZE, no_echo: Annotated[ bool, Doc("Boolean indicating should commands be echoed."), ] = False, - tls: Annotated[ - Optional["ssl.SSLContext"], - Doc("Some SSL context to make NATS connections secure."), - ] = None, tls_hostname: Annotated[ - Optional[str], + str | None, Doc("Hostname for TLS."), ] = None, - user: Annotated[ - Optional[str], - Doc("Username for NATS auth."), - ] = None, - password: Annotated[ - Optional[str], - Doc("Username password for NATS auth."), - ] = None, token: Annotated[ - Optional[str], + str | None, Doc("Auth token for NATS auth."), ] = None, drain_timeout: Annotated[ @@ -185,14 +169,14 @@ def __init__( Doc( "A callback used to sign a nonce from the server while " "authenticating with nkeys. The user should sign the nonce and " - "return the base64 encoded signature." + "return the base64 encoded signature.", ), ] = None, user_jwt_cb: Annotated[ Optional["JWTCallback"], Doc( "A callback used to fetch and return the account " - "signed JWT for this user." + "signed JWT for this user.", ), ] = None, user_credentials: Annotated[ @@ -200,17 +184,17 @@ def __init__( Doc("A user credentials file or tuple of files."), ] = None, nkeys_seed: Annotated[ - Optional[str], + str | None, Doc("Nkeys seed to be used."), ] = None, nkeys_seed_str: Annotated[ - Optional[str], + str | None, Doc("Raw nkeys seed to be used."), ] = None, inbox_prefix: Annotated[ - Union[str, bytes], + str | bytes, Doc( - "Prefix for generating unique inboxes, subjects with that prefix and NUID.ß" + "Prefix for generating unique inboxes, subjects with that prefix and NUID.ß", ), ] = DEFAULT_INBOX_PREFIX, pending_size: Annotated[ @@ -218,14 +202,14 @@ def __init__( Doc("Max size of the pending buffer for publishing commands."), ] = DEFAULT_PENDING_SIZE, flush_timeout: Annotated[ - Optional[float], + float | None, Doc("Max duration to wait for a forced flush to occur."), ] = None, # broker args graceful_timeout: Annotated[ - Optional[float], + float | None, Doc( - "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down." + "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down.", ), ] = 15.0, decoder: Annotated[ @@ -244,29 +228,29 @@ def __init__( security: Annotated[ Optional["BaseSecurity"], Doc( - "Security options to connect broker and generate AsyncAPI server security information." + "Security options to connect broker and generate AsyncAPI server security information.", ), ] = None, - asyncapi_url: Annotated[ - Union[str, Iterable[str], None], + specification_url: Annotated[ + str | Iterable[str] | None, Doc("AsyncAPI hardcoded server addresses. Use `servers` if not specified."), ] = None, protocol: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server protocol."), ] = "nats", protocol_version: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server protocol version."), ] = "custom", description: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server description."), ] = None, - asyncapi_tags: Annotated[ - Optional[Iterable[Union["asyncapi.Tag", "asyncapi.TagDict"]]], + specification_tags: Annotated[ + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], @@ -276,26 +260,18 @@ def __init__( int, Doc("Service messages log level."), ] = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - Doc("Default logger log format."), - ] = EMPTY, # StreamRouter options setup_state: Annotated[ bool, Doc( "Whether to add broker to app scope in lifespan. " - "You should disable this option at old ASGI servers." + "You should disable this option at old ASGI servers.", ), ] = True, schema_url: Annotated[ - Optional[str], + str | None, Doc( - "AsyncAPI schema url. You should set this option to `None` to disable AsyncAPI routes at all." + "AsyncAPI schema url. You should set this option to `None` to disable AsyncAPI routes at all.", ), ] = "/asyncapi", # FastAPI args @@ -304,7 +280,7 @@ def __init__( Doc("An optional path prefix for the router."), ] = "", tags: Annotated[ - Optional[List[Union[str, "Enum"]]], + list[Union[str, "Enum"]] | None, Doc( """ A list of tags to be applied to all the *path operations* in this @@ -314,11 +290,11 @@ def __init__( Read more about it in the [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). - """ + """, ), ] = None, dependencies: Annotated[ - Optional[Sequence["params.Depends"]], + Sequence["params.Depends"] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to all the @@ -326,22 +302,22 @@ def __init__( Read more about it in the [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). - """ + """, ), ] = None, default_response_class: Annotated[ - Type["Response"], + type["Response"], Doc( """ The default response class to be used. Read more in the [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). - """ + """, ), ] = Default(JSONResponse), responses: Annotated[ - Optional[Dict[Union[int, str], "AnyDict"]], + dict[int | str, "AnyDict"] | None, Doc( """ Additional responses to be shown in OpenAPI. @@ -353,11 +329,11 @@ def __init__( And in the [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). - """ + """, ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + list[BaseRoute] | None, Doc( """ OpenAPI callbacks that should apply to all *path operations* in this @@ -367,11 +343,11 @@ def __init__( Read more about it in the [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). - """ + """, ), ] = None, routes: Annotated[ - Optional[List[BaseRoute]], + list[BaseRoute] | None, Doc( """ **Note**: you probably shouldn't use this parameter, it is inherited @@ -380,7 +356,7 @@ def __init__( --- A list of routes to serve incoming HTTP and WebSocket requests. - """ + """, ), deprecated( """ @@ -389,7 +365,7 @@ def __init__( In FastAPI, you normally would use the *path operation methods*, like `router.get()`, `router.post()`, etc. - """ + """, ), ] = None, redirect_slashes: Annotated[ @@ -398,7 +374,7 @@ def __init__( """ Whether to detect and redirect slashes in URLs when the client doesn't use the same format. - """ + """, ), ] = True, default: Annotated[ @@ -407,33 +383,33 @@ def __init__( """ Default function handler for this router. Used to handle 404 Not Found errors. - """ + """, ), ] = None, dependency_overrides_provider: Annotated[ - Optional[Any], + Any | None, Doc( """ Only used internally by FastAPI to handle dependency overrides. You shouldn't need to use it. It normally points to the `FastAPI` app object. - """ + """, ), ] = None, route_class: Annotated[ - Type["APIRoute"], + type["APIRoute"], Doc( """ Custom route (*path operation*) class to be used by this router. Read more about it in the [FastAPI docs for Custom Request and APIRoute class](https://fastapi.tiangolo.com/how-to/custom-request-and-route/#custom-apiroute-class-in-a-router). - """ + """, ), ] = APIRoute, on_startup: Annotated[ - Optional[Sequence[Callable[[], Any]]], + Sequence[Callable[[], Any]] | None, Doc( """ A list of startup event handler functions. @@ -441,11 +417,11 @@ def __init__( You should instead use the `lifespan` handlers. Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ + """, ), ] = None, on_shutdown: Annotated[ - Optional[Sequence[Callable[[], Any]]], + Sequence[Callable[[], Any]] | None, Doc( """ A list of shutdown event handler functions. @@ -454,7 +430,7 @@ def __init__( Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ + """, ), ] = None, lifespan: Annotated[ @@ -466,11 +442,11 @@ def __init__( Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ + """, ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark all *path operations* in this router as deprecated. @@ -479,7 +455,7 @@ def __init__( Read more about it in the [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). - """ + """, ), ] = None, include_in_schema: Annotated[ @@ -493,7 +469,7 @@ def __init__( Read more about it in the [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). - """ + """, ), ] = True, generate_unique_id_function: Annotated[ @@ -508,7 +484,7 @@ def __init__( Read more about it in the [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). - """ + """, ), ] = Default(generate_unique_id), ) -> None: @@ -531,10 +507,7 @@ def __init__( dont_randomize=dont_randomize, flusher_queue_size=flusher_queue_size, no_echo=no_echo, - tls=tls, tls_hostname=tls_hostname, - user=user, - password=password, token=token, drain_timeout=drain_timeout, signature_cb=signature_cb, @@ -551,14 +524,13 @@ def __init__( parser=parser, middlewares=middlewares, security=security, - asyncapi_url=asyncapi_url, + specification_url=specification_url, protocol=protocol, protocol_version=protocol_version, description=description, logger=logger, log_level=log_level, - log_fmt=log_fmt, - asyncapi_tags=asyncapi_tags, + specification_tags=specification_tags, schema_url=schema_url, setup_state=setup_state, # FastAPI kwargs @@ -591,27 +563,27 @@ def subscriber( # type: ignore[override] str, Doc( "Subscribers' NATS queue name. Subscribers with same queue name will be load balanced by the NATS " - "server." + "server.", ), ] = "", pending_msgs_limit: Annotated[ - Optional[int], + int | None, Doc( "Limit of messages, considered by NATS server as possible to be delivered to the client without " "been answered. In case of NATS Core, if that limits exceeds, you will receive NATS 'Slow Consumer' " "error. " "That's literally means that your worker can't handle the whole load. In case of NATS JetStream, " - "you will no longer receive messages until some of delivered messages will be acked in any way." + "you will no longer receive messages until some of delivered messages will be acked in any way.", ), ] = None, pending_bytes_limit: Annotated[ - Optional[int], + int | None, Doc( "The number of bytes, considered by NATS server as possible to be delivered to the client without " "been answered. In case of NATS Core, if that limit exceeds, you will receive NATS 'Slow Consumer' " "error." "That's literally means that your worker can't handle the whole load. In case of NATS JetStream, " - "you will no longer receive messages until some of delivered messages will be acked in any way." + "you will no longer receive messages until some of delivered messages will be acked in any way.", ), ] = None, # Core arguments @@ -621,9 +593,9 @@ def subscriber( # type: ignore[override] ] = 0, # JS arguments durable: Annotated[ - Optional[str], + str | None, Doc( - "Name of the durable consumer to which the the subscription should be bound." + "Name of the durable consumer to which the the subscription should be bound.", ), ] = None, config: Annotated[ @@ -635,11 +607,11 @@ def subscriber( # type: ignore[override] Doc("Enable ordered consumer mode."), ] = False, idle_heartbeat: Annotated[ - Optional[float], + float | None, Doc("Enable Heartbeats for a consumer to detect failures."), ] = None, flow_control: Annotated[ - Optional[bool], + bool | None, Doc("Enable Flow Control for a consumer."), ] = None, deliver_policy: Annotated[ @@ -647,9 +619,9 @@ def subscriber( # type: ignore[override] Doc("Deliver Policy to be used for subscription."), ] = None, headers_only: Annotated[ - Optional[bool], + bool | None, Doc( - "Should be message delivered without payload, only headers and metadata." + "Should be message delivered without payload, only headers and metadata.", ), ] = None, # pull arguments @@ -657,7 +629,7 @@ def subscriber( # type: ignore[override] Optional["PullSub"], Doc( "NATS Pull consumer parameters container. " - "Should be used with `stream` only." + "Should be used with `stream` only.", ), ] = None, kv_watch: Annotated[ @@ -671,14 +643,20 @@ def subscriber( # type: ignore[override] inbox_prefix: Annotated[ bytes, Doc( - "Prefix for generating unique inboxes, subjects with that prefix and NUID." + "Prefix for generating unique inboxes, subjects with that prefix and NUID.", ), ] = api.INBOX_PREFIX, # custom ack_first: Annotated[ bool, Doc("Whether to `ack` message at start of consuming or not."), - ] = False, + deprecated( + """ + This option is deprecated and will be removed in 0.7.0 release. + Please, use `ack_policy=AckPolicy.ACK_FIRST` instead. + """, + ), + ] = EMPTY, stream: Annotated[ Union[str, "JStream", None], Doc("Subscribe to NATS Stream with `subject` filter."), @@ -698,50 +676,41 @@ def subscriber( # type: ignore[override] ] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[NatsMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - Union[ - "Filter[NatsMessage]", - "Filter[NatsBatchMessage]", - ], - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = default_filter, + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), max_workers: Annotated[ int, Doc("Number of workers to process messages concurrently."), ] = 1, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, no_ack: Annotated[ bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), + deprecated( + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" + ), + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, # AsyncAPI information title: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ @@ -780,7 +749,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). - """ + """, ), ] = Default(None), response_model_include: Annotated[ @@ -792,7 +761,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_exclude: Annotated[ @@ -804,7 +773,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_by_alias: Annotated[ @@ -816,7 +785,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = True, response_model_exclude_unset: Annotated[ @@ -834,7 +803,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_defaults: Annotated[ @@ -851,7 +820,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_none: Annotated[ @@ -868,12 +837,12 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). - """ + """, ), ] = False, - ) -> "AsyncAPISubscriber": + ) -> "SpecificationSubscriber": return cast( - "AsyncAPISubscriber", + "SpecificationSubscriber", super().subscriber( subject=subject, queue=queue, @@ -896,9 +865,8 @@ def subscriber( # type: ignore[override] parser=parser, decoder=decoder, middlewares=middlewares, - filter=filter, max_workers=max_workers, - retry=retry, + ack_policy=ack_policy, no_ack=no_ack, no_reply=no_reply, title=title, @@ -924,11 +892,11 @@ def publisher( # type: ignore[override] Doc("NATS subject to send message."), ], headers: Annotated[ - Optional[Dict[str, str]], + dict[str, str] | None, Doc( "Message headers to store metainformation. " "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -940,39 +908,43 @@ def publisher( # type: ignore[override] Union[str, "JStream", None], Doc( "This option validates that the target `subject` is in presented stream. " - "Can be omitted without any effect." + "Can be omitted without any effect.", ), ] = None, timeout: Annotated[ - Optional[float], + float | None, Doc("Timeout to send message to NATS."), ] = None, # specific middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), # AsyncAPI information title: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI publisher object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, Doc("Whetever to include operation in AsyncAPI schema or not."), ] = True, - ) -> AsyncAPIPublisher: + ) -> "LogicPublisher": return self.broker.publisher( subject, headers=headers, diff --git a/faststream/nats/helpers/bucket_declarer.py b/faststream/nats/helpers/bucket_declarer.py index 916b706254..a5b8e3ac99 100644 --- a/faststream/nats/helpers/bucket_declarer.py +++ b/faststream/nats/helpers/bucket_declarer.py @@ -1,7 +1,9 @@ -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING, Optional from nats.js.api import KeyValueConfig +from .state import ConnectedState, ConnectionState, EmptyConnectionState + if TYPE_CHECKING: from nats.js import JetStreamContext from nats.js.api import Placement, RePublish, StorageType @@ -9,32 +11,39 @@ class KVBucketDeclarer: - buckets: Dict[str, "KeyValue"] + buckets: dict[str, "KeyValue"] - def __init__(self, connection: "JetStreamContext") -> None: - self._connection = connection + def __init__(self) -> None: self.buckets = {} + self.__state: ConnectionState[JetStreamContext] = EmptyConnectionState() + + def connect(self, connection: "JetStreamContext") -> None: + self.__state = ConnectedState(connection) + + def disconnect(self) -> None: + self.__state = EmptyConnectionState() + async def create_key_value( self, bucket: str, *, - description: Optional[str] = None, - max_value_size: Optional[int] = None, + description: str | None = None, + max_value_size: int | None = None, history: int = 1, - ttl: Optional[float] = None, # in seconds - max_bytes: Optional[int] = None, + ttl: float | None = None, # in seconds + max_bytes: int | None = None, storage: Optional["StorageType"] = None, replicas: int = 1, placement: Optional["Placement"] = None, republish: Optional["RePublish"] = None, - direct: Optional[bool] = None, + direct: bool | None = None, # custom declare: bool = True, ) -> "KeyValue": if (key_value := self.buckets.get(bucket)) is None: if declare: - key_value = await self._connection.create_key_value( + key_value = await self.__state.connection.create_key_value( config=KeyValueConfig( bucket=bucket, description=description, @@ -47,10 +56,10 @@ async def create_key_value( placement=placement, republish=republish, direct=direct, - ) + ), ) else: - key_value = await self._connection.key_value(bucket) + key_value = await self.__state.connection.key_value(bucket) self.buckets[bucket] = key_value diff --git a/faststream/nats/helpers/obj_storage_declarer.py b/faststream/nats/helpers/obj_storage_declarer.py index 1d2ae50715..22a4642d98 100644 --- a/faststream/nats/helpers/obj_storage_declarer.py +++ b/faststream/nats/helpers/obj_storage_declarer.py @@ -1,7 +1,9 @@ -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING, Optional from nats.js.api import ObjectStoreConfig +from .state import ConnectedState, ConnectionState, EmptyConnectionState + if TYPE_CHECKING: from nats.js import JetStreamContext from nats.js.api import Placement, StorageType @@ -9,19 +11,26 @@ class OSBucketDeclarer: - buckets: Dict[str, "ObjectStore"] + buckets: dict[str, "ObjectStore"] - def __init__(self, connection: "JetStreamContext") -> None: - self._connection = connection + def __init__(self) -> None: self.buckets = {} + self.__state: ConnectionState[JetStreamContext] = EmptyConnectionState() + + def connect(self, connection: "JetStreamContext") -> None: + self.__state = ConnectedState(connection) + + def disconnect(self) -> None: + self.__state = EmptyConnectionState() + async def create_object_store( self, bucket: str, *, - description: Optional[str] = None, - ttl: Optional[float] = None, - max_bytes: Optional[int] = None, + description: str | None = None, + ttl: float | None = None, + max_bytes: int | None = None, storage: Optional["StorageType"] = None, replicas: int = 1, placement: Optional["Placement"] = None, @@ -30,7 +39,7 @@ async def create_object_store( ) -> "ObjectStore": if (object_store := self.buckets.get(bucket)) is None: if declare: - object_store = await self._connection.create_object_store( + object_store = await self.__state.connection.create_object_store( bucket=bucket, config=ObjectStoreConfig( bucket=bucket, @@ -43,7 +52,7 @@ async def create_object_store( ), ) else: - object_store = await self._connection.object_store(bucket) + object_store = await self.__state.connection.object_store(bucket) self.buckets[bucket] = object_store diff --git a/faststream/nats/helpers/object_builder.py b/faststream/nats/helpers/object_builder.py index 5d40a44da6..7e9fb86f13 100644 --- a/faststream/nats/helpers/object_builder.py +++ b/faststream/nats/helpers/object_builder.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, Union +from typing import Optional, Union from faststream.nats.schemas import JStream @@ -8,7 +8,7 @@ class StreamBuilder: __slots__ = ("objects",) - objects: Dict[str, "JStream"] + objects: dict[str, "JStream"] def __init__(self) -> None: """Initialize the builder.""" diff --git a/faststream/nats/helpers/state.py b/faststream/nats/helpers/state.py new file mode 100644 index 0000000000..57e371876b --- /dev/null +++ b/faststream/nats/helpers/state.py @@ -0,0 +1,28 @@ +from typing import Protocol, TypeVar + +from nats.aio.client import Client +from nats.js import JetStreamContext +from typing_extensions import ReadOnly + +from faststream.exceptions import IncorrectState + +ClientT = TypeVar("ClientT", Client, JetStreamContext) + + +class ConnectionState(Protocol[ClientT]): + connection: ReadOnly[ClientT] + + +class EmptyConnectionState(ConnectionState[ClientT]): + __slots__ = () + + @property + def connection(self) -> ClientT: + raise IncorrectState + + +class ConnectedState(ConnectionState[ClientT]): + __slots__ = ("connection",) + + def __init__(self, connection: ClientT) -> None: + self.connection = connection diff --git a/faststream/nats/message.py b/faststream/nats/message.py index 5e5d89fd86..d281529f03 100644 --- a/faststream/nats/message.py +++ b/faststream/nats/message.py @@ -1,10 +1,9 @@ -from typing import List, Union from nats.aio.msg import Msg from nats.js.api import ObjectInfo from nats.js.kv import KeyValue -from faststream.broker.message import StreamMessage +from faststream.message import StreamMessage class NatsMessage(StreamMessage[Msg]): @@ -24,7 +23,7 @@ async def ack_sync(self) -> None: async def nack( self, - delay: Union[int, float, None] = None, + delay: float | None = None, ) -> None: if not self.raw_message._ackd: await self.raw_message.nak(delay=delay) @@ -40,7 +39,7 @@ async def in_progress(self) -> None: await self.raw_message.in_progress() -class NatsBatchMessage(StreamMessage[List[Msg]]): +class NatsBatchMessage(StreamMessage[list[Msg]]): """A class to represent a NATS batch message.""" async def ack(self) -> None: @@ -54,7 +53,7 @@ async def ack(self) -> None: async def nack( self, - delay: Union[int, float, None] = None, + delay: float | None = None, ) -> None: for m in filter( lambda m: not m._ackd, diff --git a/faststream/nats/opentelemetry/middleware.py b/faststream/nats/opentelemetry/middleware.py index cafd8787d8..cef0dffcb8 100644 --- a/faststream/nats/opentelemetry/middleware.py +++ b/faststream/nats/opentelemetry/middleware.py @@ -1,19 +1,19 @@ -from typing import Optional from opentelemetry.metrics import Meter, MeterProvider from opentelemetry.trace import TracerProvider from faststream.nats.opentelemetry.provider import telemetry_attributes_provider_factory +from faststream.nats.response import NatsPublishCommand from faststream.opentelemetry.middleware import TelemetryMiddleware -class NatsTelemetryMiddleware(TelemetryMiddleware): +class NatsTelemetryMiddleware(TelemetryMiddleware[NatsPublishCommand]): def __init__( self, *, - tracer_provider: Optional[TracerProvider] = None, - meter_provider: Optional[MeterProvider] = None, - meter: Optional[Meter] = None, + tracer_provider: TracerProvider | None = None, + meter_provider: MeterProvider | None = None, + meter: Meter | None = None, ) -> None: super().__init__( settings_provider_factory=telemetry_attributes_provider_factory, diff --git a/faststream/nats/opentelemetry/provider.py b/faststream/nats/opentelemetry/provider.py index a77ff0a2b3..e44a42bd14 100644 --- a/faststream/nats/opentelemetry/provider.py +++ b/faststream/nats/opentelemetry/provider.py @@ -1,16 +1,17 @@ -from typing import TYPE_CHECKING, List, Optional, Sequence, Union, overload +from collections.abc import Sequence +from typing import TYPE_CHECKING, Optional, Union, overload from nats.aio.msg import Msg from opentelemetry.semconv.trace import SpanAttributes -from faststream.__about__ import SERVICE_NAME -from faststream.broker.types import MsgType +from faststream._internal.types import MsgType from faststream.opentelemetry import TelemetrySettingsProvider from faststream.opentelemetry.consts import MESSAGING_DESTINATION_PUBLISH_NAME if TYPE_CHECKING: - from faststream.broker.message import StreamMessage - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict + from faststream.message import StreamMessage + from faststream.response import PublishCommand class BaseNatsTelemetrySettingsProvider(TelemetrySettingsProvider[MsgType]): @@ -19,22 +20,21 @@ class BaseNatsTelemetrySettingsProvider(TelemetrySettingsProvider[MsgType]): def __init__(self) -> None: self.messaging_system = "nats" - def get_publish_attrs_from_kwargs( + def get_publish_attrs_from_cmd( self, - kwargs: "AnyDict", + cmd: "PublishCommand", ) -> "AnyDict": return { SpanAttributes.MESSAGING_SYSTEM: self.messaging_system, - SpanAttributes.MESSAGING_DESTINATION_NAME: kwargs["subject"], - SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: kwargs["correlation_id"], + SpanAttributes.MESSAGING_DESTINATION_NAME: cmd.destination, + SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: cmd.correlation_id, } def get_publish_destination_name( self, - kwargs: "AnyDict", + cmd: "PublishCommand", ) -> str: - subject: str = kwargs.get("subject", SERVICE_NAME) - return subject + return cmd.destination class NatsTelemetrySettingsProvider(BaseNatsTelemetrySettingsProvider["Msg"]): @@ -58,11 +58,11 @@ def get_consume_destination_name( class NatsBatchTelemetrySettingsProvider( - BaseNatsTelemetrySettingsProvider[List["Msg"]] + BaseNatsTelemetrySettingsProvider[list["Msg"]], ): def get_consume_attrs_from_message( self, - msg: "StreamMessage[List[Msg]]", + msg: "StreamMessage[list[Msg]]", ) -> "AnyDict": return { SpanAttributes.MESSAGING_SYSTEM: self.messaging_system, @@ -75,7 +75,7 @@ def get_consume_attrs_from_message( def get_consume_destination_name( self, - msg: "StreamMessage[List[Msg]]", + msg: "StreamMessage[list[Msg]]", ) -> str: return msg.raw_message[0].subject @@ -95,23 +95,15 @@ def telemetry_attributes_provider_factory( @overload def telemetry_attributes_provider_factory( msg: Union["Msg", Sequence["Msg"], None], -) -> Union[ - NatsTelemetrySettingsProvider, - NatsBatchTelemetrySettingsProvider, -]: ... +) -> NatsTelemetrySettingsProvider | NatsBatchTelemetrySettingsProvider: ... def telemetry_attributes_provider_factory( msg: Union["Msg", Sequence["Msg"], None], -) -> Union[ - NatsTelemetrySettingsProvider, - NatsBatchTelemetrySettingsProvider, - None, -]: +) -> NatsTelemetrySettingsProvider | NatsBatchTelemetrySettingsProvider | None: if isinstance(msg, Sequence): return NatsBatchTelemetrySettingsProvider() - elif isinstance(msg, Msg) or msg is None: + if isinstance(msg, Msg) or msg is None: return NatsTelemetrySettingsProvider() - else: - # KeyValue and Object Storage watch cases - return None + # KeyValue and Object Storage watch cases + return None diff --git a/faststream/nats/parser.py b/faststream/nats/parser.py index c3a1c32928..af9a8e73d3 100644 --- a/faststream/nats/parser.py +++ b/faststream/nats/parser.py @@ -1,6 +1,9 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Optional -from faststream.broker.message import StreamMessage, decode_message, gen_cor_id +from faststream.message import ( + StreamMessage, + decode_message, +) from faststream.nats.message import ( NatsBatchMessage, NatsKvMessage, @@ -14,7 +17,7 @@ from nats.js.api import ObjectInfo from nats.js.kv import KeyValue - from faststream.types import AnyDict, DecodedMessage + from faststream._internal.basic_types import AnyDict, DecodedMessage class NatsBaseParser: @@ -32,7 +35,7 @@ def get_path( self, subject: str, ) -> Optional["AnyDict"]: - path: Optional[AnyDict] = None + path: AnyDict | None = None if (path_re := self.__path_re) is not None and ( match := path_re.match(subject) @@ -51,9 +54,10 @@ async def decode_message( class NatsParser(NatsBaseParser): """A class to parse NATS core messages.""" - def __init__(self, *, pattern: str, no_ack: bool) -> None: + def __init__(self, *, pattern: str, is_ack_disabled: bool) -> None: super().__init__(pattern=pattern) - self.no_ack = no_ack + + self.is_ack_disabled = is_ack_disabled async def parse_message( self, @@ -66,8 +70,8 @@ async def parse_message( headers = message.header or {} - if not self.no_ack: - message._ackd = True # prevent message from acking + if self.is_ack_disabled: + message._ackd = True return NatsMessage( raw_message=message, @@ -76,8 +80,8 @@ async def parse_message( reply_to=message.reply, headers=headers, content_type=headers.get("content-type", ""), - message_id=headers.get("message_id", gen_cor_id()), - correlation_id=headers.get("correlation_id", gen_cor_id()), + message_id=headers.get("message_id"), + correlation_id=headers.get("correlation_id"), ) @@ -101,9 +105,9 @@ async def parse_message( path=path or {}, reply_to=headers.get("reply_to", ""), # differ from core headers=headers, - content_type=headers.get("content-type", ""), - message_id=headers.get("message_id", gen_cor_id()), - correlation_id=headers.get("correlation_id", gen_cor_id()), + content_type=headers.get("content-type"), + message_id=headers.get("message_id"), + correlation_id=headers.get("correlation_id"), ) @@ -112,10 +116,10 @@ class BatchParser(JsParser): async def parse_batch( self, - message: List["Msg"], - ) -> "StreamMessage[List[Msg]]": - body: List[bytes] = [] - batch_headers: List[Dict[str, str]] = [] + message: list["Msg"], + ) -> "StreamMessage[list[Msg]]": + body: list[bytes] = [] + batch_headers: list[dict[str, str]] = [] if message: path = self.get_path(message[0].subject) @@ -139,11 +143,11 @@ async def parse_batch( async def decode_batch( self, - msg: "StreamMessage[List[Msg]]", - ) -> List["DecodedMessage"]: - data: List[DecodedMessage] = [] + msg: "StreamMessage[list[Msg]]", + ) -> list["DecodedMessage"]: + data: list[DecodedMessage] = [] - path: Optional[AnyDict] = None + path: AnyDict | None = None for m in msg.raw_message: one_msg = await self.parse_message(m, path=path) path = one_msg.path @@ -155,7 +159,8 @@ async def decode_batch( class KvParser(NatsBaseParser): async def parse_message( - self, msg: "KeyValue.Entry" + self, + msg: "KeyValue.Entry", ) -> StreamMessage["KeyValue.Entry"]: return NatsKvMessage( raw_message=msg, diff --git a/faststream/nats/prometheus/middleware.py b/faststream/nats/prometheus/middleware.py index 3aadeb61d1..3671b9d21f 100644 --- a/faststream/nats/prometheus/middleware.py +++ b/faststream/nats/prometheus/middleware.py @@ -1,24 +1,30 @@ -from typing import TYPE_CHECKING, Optional, Sequence +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union +from nats.aio.msg import Msg + +from faststream._internal.constants import EMPTY from faststream.nats.prometheus.provider import settings_provider_factory -from faststream.prometheus.middleware import BasePrometheusMiddleware -from faststream.types import EMPTY +from faststream.nats.response import NatsPublishCommand +from faststream.prometheus.middleware import PrometheusMiddleware if TYPE_CHECKING: from prometheus_client import CollectorRegistry -class NatsPrometheusMiddleware(BasePrometheusMiddleware): +class NatsPrometheusMiddleware( + PrometheusMiddleware[NatsPublishCommand, Union[Msg, Sequence[Msg]]] +): def __init__( self, *, registry: "CollectorRegistry", app_name: str = EMPTY, metrics_prefix: str = "faststream", - received_messages_size_buckets: Optional[Sequence[float]] = None, + received_messages_size_buckets: Sequence[float] | None = None, ) -> None: super().__init__( - settings_provider_factory=settings_provider_factory, + settings_provider_factory=settings_provider_factory, # type: ignore[arg-type] registry=registry, app_name=app_name, metrics_prefix=metrics_prefix, diff --git a/faststream/nats/prometheus/provider.py b/faststream/nats/prometheus/provider.py index 6247e870b3..5d4915bfdb 100644 --- a/faststream/nats/prometheus/provider.py +++ b/faststream/nats/prometheus/provider.py @@ -1,15 +1,16 @@ -from typing import TYPE_CHECKING, List, Sequence, Union, cast +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union from nats.aio.msg import Msg -from faststream.broker.message import MsgType, StreamMessage +from faststream.message.message import MsgType, StreamMessage from faststream.prometheus import ( ConsumeAttrs, MetricsSettingsProvider, ) if TYPE_CHECKING: - from faststream.types import AnyDict + from faststream.response import PublishCommand class BaseNatsMetricsSettingsProvider(MetricsSettingsProvider[MsgType]): @@ -18,11 +19,11 @@ class BaseNatsMetricsSettingsProvider(MetricsSettingsProvider[MsgType]): def __init__(self) -> None: self.messaging_system = "nats" - def get_publish_destination_name_from_kwargs( + def get_publish_destination_name_from_cmd( self, - kwargs: "AnyDict", + cmd: "PublishCommand", ) -> str: - return cast("str", kwargs["subject"]) + return cmd.destination class NatsMetricsSettingsProvider(BaseNatsMetricsSettingsProvider["Msg"]): @@ -37,10 +38,10 @@ def get_consume_attrs_from_message( } -class BatchNatsMetricsSettingsProvider(BaseNatsMetricsSettingsProvider[List["Msg"]]): +class BatchNatsMetricsSettingsProvider(BaseNatsMetricsSettingsProvider[list["Msg"]]): def get_consume_attrs_from_message( self, - msg: "StreamMessage[List[Msg]]", + msg: "StreamMessage[list[Msg]]", ) -> ConsumeAttrs: raw_message = msg.raw_message[0] return { @@ -52,15 +53,10 @@ def get_consume_attrs_from_message( def settings_provider_factory( msg: Union["Msg", Sequence["Msg"], None], -) -> Union[ - NatsMetricsSettingsProvider, - BatchNatsMetricsSettingsProvider, - None, -]: +) -> NatsMetricsSettingsProvider | BatchNatsMetricsSettingsProvider | None: if isinstance(msg, Sequence): return BatchNatsMetricsSettingsProvider() - elif isinstance(msg, Msg) or msg is None: + if isinstance(msg, Msg) or msg is None: return NatsMetricsSettingsProvider() - else: - # KeyValue and Object Storage watch cases - return None + # KeyValue and Object Storage watch cases + return None diff --git a/faststream/nats/publisher/asyncapi.py b/faststream/nats/publisher/asyncapi.py deleted file mode 100644 index 7ce50295d7..0000000000 --- a/faststream/nats/publisher/asyncapi.py +++ /dev/null @@ -1,85 +0,0 @@ -from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence - -from typing_extensions import override - -from faststream.asyncapi.schema import ( - Channel, - ChannelBinding, - CorrelationId, - Message, - Operation, -) -from faststream.asyncapi.schema.bindings import nats -from faststream.asyncapi.utils import resolve_payloads -from faststream.nats.publisher.usecase import LogicPublisher - -if TYPE_CHECKING: - from nats.aio.msg import Msg - - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware - from faststream.nats.schemas.js_stream import JStream - - -class AsyncAPIPublisher(LogicPublisher): - """A class to represent a NATS publisher.""" - - def get_name(self) -> str: - return f"{self.subject}:Publisher" - - def get_schema(self) -> Dict[str, Channel]: - payloads = self.get_payloads() - - return { - self.name: Channel( - description=self.description, - publish=Operation( - message=Message( - title=f"{self.name}:Message", - payload=resolve_payloads(payloads, "Publisher"), - correlationId=CorrelationId( - location="$message.header#/correlation_id" - ), - ), - ), - bindings=ChannelBinding( - nats=nats.ChannelBinding( - subject=self.subject, - ) - ), - ) - } - - @override - @classmethod - def create( # type: ignore[override] - cls, - *, - subject: str, - reply_to: str, - headers: Optional[Dict[str, str]], - stream: Optional["JStream"], - timeout: Optional[float], - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[Msg]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> "AsyncAPIPublisher": - return cls( - subject=subject, - reply_to=reply_to, - headers=headers, - stream=stream, - timeout=timeout, - # Publisher args - broker_middlewares=broker_middlewares, - middlewares=middlewares, - # AsyncAPI args - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) diff --git a/faststream/nats/publisher/config.py b/faststream/nats/publisher/config.py new file mode 100644 index 0000000000..6fdc4f8468 --- /dev/null +++ b/faststream/nats/publisher/config.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Optional + +from faststream._internal.configs import ( + PublisherSpecificationConfig, + PublisherUsecaseConfig, +) +from faststream.nats.configs import NatsBrokerConfig + +if TYPE_CHECKING: + from faststream.nats.schemas import JStream + + +@dataclass(kw_only=True) +class NatsPublisherSpecificationConfig(PublisherSpecificationConfig): + subject: str + + +@dataclass(kw_only=True) +class NatsPublisherConfig(PublisherUsecaseConfig): + _outer_config: "NatsBrokerConfig" = field(default_factory=NatsBrokerConfig) + + subject: str + reply_to: str + headers: dict[str, str] | None + stream: Optional["JStream"] + timeout: float | None diff --git a/faststream/nats/publisher/factory.py b/faststream/nats/publisher/factory.py new file mode 100644 index 0000000000..623c8eea7b --- /dev/null +++ b/faststream/nats/publisher/factory.py @@ -0,0 +1,51 @@ +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Optional + +from .config import NatsPublisherConfig, NatsPublisherSpecificationConfig +from .specification import NatsPublisherSpecification +from .usecase import LogicPublisher + +if TYPE_CHECKING: + from faststream._internal.types import PublisherMiddleware + from faststream.nats.configs import NatsBrokerConfig + from faststream.nats.schemas.js_stream import JStream + + +def create_publisher( + *, + subject: str, + reply_to: str, + headers: dict[str, str] | None, + stream: Optional["JStream"], + timeout: float | None, + # Publisher args + broker_config: "NatsBrokerConfig", + middlewares: Sequence["PublisherMiddleware"], + # AsyncAPI args + schema_: Any | None, + title_: str | None, + description_: str | None, + include_in_schema: bool, +) -> LogicPublisher: + publisher_config = NatsPublisherConfig( + subject=subject, + stream=stream, + reply_to=reply_to, + headers=headers, + timeout=timeout, + middlewares=middlewares, + _outer_config=broker_config, + ) + + specification = NatsPublisherSpecification( + _outer_config=broker_config, + specification_config=NatsPublisherSpecificationConfig( + subject=subject, + schema_=schema_, + title_=title_, + description_=description_, + include_in_schema=include_in_schema, + ), + ) + + return LogicPublisher(publisher_config, specification) diff --git a/faststream/nats/publisher/fake.py b/faststream/nats/publisher/fake.py new file mode 100644 index 0000000000..9e98859a8f --- /dev/null +++ b/faststream/nats/publisher/fake.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING, Union + +from faststream._internal.endpoint.publisher.fake import FakePublisher +from faststream.nats.response import NatsPublishCommand + +if TYPE_CHECKING: + from faststream._internal.producer import ProducerProto + from faststream.response.response import PublishCommand + + +class NatsFakePublisher(FakePublisher): + """Publisher Interface implementation to use as RPC or REPLY TO answer publisher.""" + + def __init__( + self, + producer: "ProducerProto", + subject: str, + ) -> None: + super().__init__(producer=producer) + self.subject = subject + + def patch_command( + self, cmd: Union["PublishCommand", "NatsPublishCommand"] + ) -> "NatsPublishCommand": + cmd = super().patch_command(cmd) + real_cmd = NatsPublishCommand.from_cmd(cmd) + real_cmd.destination = self.subject + return real_cmd diff --git a/faststream/nats/publisher/producer.py b/faststream/nats/publisher/producer.py index eedb27932f..bbf0eded5d 100644 --- a/faststream/nats/publisher/producer.py +++ b/faststream/nats/publisher/producer.py @@ -1,30 +1,62 @@ import asyncio -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Optional import anyio import nats from typing_extensions import override -from faststream.broker.message import encode_message -from faststream.broker.publisher.proto import ProducerProto -from faststream.broker.utils import resolve_custom_func -from faststream.exceptions import WRONG_PUBLISH_ARGS +from faststream._internal.endpoint.utils import resolve_custom_func +from faststream._internal.producer import ProducerProto +from faststream.exceptions import FeatureNotSupportedException +from faststream.message import encode_message +from faststream.nats.helpers.state import ( + ConnectedState, + ConnectionState, + EmptyConnectionState, +) from faststream.nats.parser import NatsParser -from faststream.utils.functions import timeout_scope if TYPE_CHECKING: + from fast_depends.library.serializer import SerializerProto from nats.aio.client import Client from nats.aio.msg import Msg from nats.js import JetStreamContext - from faststream.broker.types import ( + from faststream._internal.types import ( AsyncCallable, CustomCallable, ) - from faststream.types import SendableMessage + from faststream.nats.response import NatsPublishCommand + from faststream.nats.schemas import PubAck class NatsFastProducer(ProducerProto): + def connect( + self, connection: "Client", serializer: Optional["SerializerProto"] + ) -> None: ... + + def disconnect(self) -> None: ... + + @override + async def publish( # type: ignore[override] + self, + cmd: "NatsPublishCommand", + ) -> None: ... + + @override + async def request( # type: ignore[override] + self, + cmd: "NatsPublishCommand", + ) -> "Msg": ... + + @override + async def publish_batch( + self, + cmd: "NatsPublishCommand", + ) -> None: ... + + +class NatsFastProducerImpl(NatsFastProducer): """A class to represent a NATS producer.""" _decoder: "AsyncCallable" @@ -32,101 +64,74 @@ class NatsFastProducer(ProducerProto): def __init__( self, - *, - connection: "Client", parser: Optional["CustomCallable"], decoder: Optional["CustomCallable"], ) -> None: - self._connection = connection + self.serializer: SerializerProto | None = None - default = NatsParser(pattern="", no_ack=False) + default = NatsParser(pattern="", is_ack_disabled=True) self._parser = resolve_custom_func(parser, default.parse_message) self._decoder = resolve_custom_func(decoder, default.decode_message) + self.__state: ConnectionState[Client] = EmptyConnectionState() + + def connect( + self, connection: "Client", serializer: Optional["SerializerProto"] + ) -> None: + self.serializer = serializer + self.__state = ConnectedState(connection) + + def disconnect(self) -> None: + self.__state = EmptyConnectionState() + @override async def publish( # type: ignore[override] self, - message: "SendableMessage", - subject: str, - *, - correlation_id: str, - headers: Optional[Dict[str, str]] = None, - reply_to: str = "", - rpc: bool = False, - rpc_timeout: Optional[float] = 30.0, - raise_timeout: bool = False, - **kwargs: Any, # suprress stream option - ) -> Optional[Any]: - payload, content_type = encode_message(message) + cmd: "NatsPublishCommand", + ) -> None: + payload, content_type = encode_message(cmd.body, self.serializer) headers_to_send = { "content-type": content_type or "", - "correlation_id": correlation_id, - **(headers or {}), + **cmd.headers_to_publish(), } - client = self._connection - - if rpc: - if reply_to: - raise WRONG_PUBLISH_ARGS - - reply_to = client.new_inbox() - - future: asyncio.Future[Msg] = asyncio.Future() - sub = await client.subscribe(reply_to, future=future, max_msgs=1) - await sub.unsubscribe(limit=1) - - await client.publish( - subject=subject, + return await self.__state.connection.publish( + subject=cmd.destination, payload=payload, - reply=reply_to, + reply=cmd.reply_to, headers=headers_to_send, ) - if rpc: - msg: Any = None - with timeout_scope(rpc_timeout, raise_timeout): - msg = await future - - if msg: # pragma: no branch - if msg.headers: # pragma: no cover # noqa: SIM102 - if ( - msg.headers.get(nats.js.api.Header.STATUS) - == nats.aio.client.NO_RESPONDERS_STATUS - ): - raise nats.errors.NoRespondersError - return await self._decoder(await self._parser(msg)) - - return None - @override async def request( # type: ignore[override] self, - message: "SendableMessage", - subject: str, - *, - correlation_id: str, - headers: Optional[Dict[str, str]] = None, - timeout: float = 0.5, + cmd: "NatsPublishCommand", ) -> "Msg": - payload, content_type = encode_message(message) + payload, content_type = encode_message(cmd.body, self.serializer) headers_to_send = { "content-type": content_type or "", - "correlation_id": correlation_id, - **(headers or {}), + **cmd.headers_to_publish(), } - return await self._connection.request( - subject=subject, + return await self.__state.connection.request( + subject=cmd.destination, payload=payload, headers=headers_to_send, - timeout=timeout, + timeout=cmd.timeout, ) + @override + async def publish_batch( + self, + cmd: "NatsPublishCommand", + ) -> None: + msg = "NATS doesn't support publishing in batches." + raise FeatureNotSupportedException(msg) + -class NatsJSFastProducer(ProducerProto): +class NatsJSFastProducer(NatsFastProducer): """A class to represent a NATS JetStream producer.""" _decoder: "AsyncCallable" @@ -135,108 +140,73 @@ class NatsJSFastProducer(ProducerProto): def __init__( self, *, - connection: "JetStreamContext", parser: Optional["CustomCallable"], decoder: Optional["CustomCallable"], ) -> None: - self._connection = connection + self.serializer: SerializerProto | None = None - default = NatsParser(pattern="", no_ack=False) + default = NatsParser(pattern="", is_ack_disabled=True) self._parser = resolve_custom_func(parser, default.parse_message) self._decoder = resolve_custom_func(decoder, default.decode_message) + self.__state: ConnectionState[JetStreamContext] = EmptyConnectionState() + + def connect( + self, connection: "JetStreamContext", serializer: Optional["SerializerProto"] + ) -> None: + self.serializer = serializer + self.__state = ConnectedState(connection) + + def disconnect(self) -> None: + self.__state = EmptyConnectionState() + @override async def publish( # type: ignore[override] self, - message: "SendableMessage", - subject: str, - *, - correlation_id: str, - headers: Optional[Dict[str, str]] = None, - reply_to: str = "", - stream: Optional[str] = None, - timeout: Optional[float] = None, - rpc: bool = False, - rpc_timeout: Optional[float] = 30.0, - raise_timeout: bool = False, - ) -> Optional[Any]: - payload, content_type = encode_message(message) + cmd: "NatsPublishCommand", + ) -> "PubAck": + payload, content_type = encode_message(cmd.body, self.serializer) headers_to_send = { "content-type": content_type or "", - "correlation_id": correlation_id, - **(headers or {}), + **cmd.headers_to_publish(js=True), } - if rpc: - if reply_to: - raise WRONG_PUBLISH_ARGS - reply_to = self._connection._nc.new_inbox() - future: asyncio.Future[Msg] = asyncio.Future() - sub = await self._connection._nc.subscribe( - reply_to, future=future, max_msgs=1 - ) - await sub.unsubscribe(limit=1) - - if reply_to: - headers_to_send.update({"reply_to": reply_to}) - - await self._connection.publish( - subject=subject, + return await self.__state.connection.publish( + subject=cmd.destination, payload=payload, headers=headers_to_send, - stream=stream, - timeout=timeout, + stream=cmd.stream, + timeout=cmd.timeout, ) - if rpc: - msg: Any = None - with timeout_scope(rpc_timeout, raise_timeout): - msg = await future - - if msg: # pragma: no branch - if msg.headers: # pragma: no cover # noqa: SIM102 - if ( - msg.headers.get(nats.js.api.Header.STATUS) - == nats.aio.client.NO_RESPONDERS_STATUS - ): - raise nats.errors.NoRespondersError - return await self._decoder(await self._parser(msg)) - - return None - @override async def request( # type: ignore[override] self, - message: "SendableMessage", - subject: str, - *, - correlation_id: str, - headers: Optional[Dict[str, str]] = None, - stream: Optional[str] = None, - timeout: float = 0.5, + cmd: "NatsPublishCommand", ) -> "Msg": - payload, content_type = encode_message(message) + payload, content_type = encode_message(cmd.body, self.serializer) - reply_to = self._connection._nc.new_inbox() + reply_to = self.__state.connection._nc.new_inbox() future: asyncio.Future[Msg] = asyncio.Future() - sub = await self._connection._nc.subscribe(reply_to, future=future, max_msgs=1) + sub = await self.__state.connection._nc.subscribe( + reply_to, future=future, max_msgs=1 + ) await sub.unsubscribe(limit=1) headers_to_send = { "content-type": content_type or "", - "correlation_id": correlation_id, "reply_to": reply_to, - **(headers or {}), + **cmd.headers_to_publish(js=False), } - with anyio.fail_after(timeout): - await self._connection.publish( - subject=subject, + with anyio.fail_after(cmd.timeout): + await self.__state.connection.publish( + subject=cmd.destination, payload=payload, headers=headers_to_send, - stream=stream, - timeout=timeout, + stream=cmd.stream, + timeout=cmd.timeout, ) msg = await future @@ -251,3 +221,40 @@ async def request( # type: ignore[override] raise nats.errors.NoRespondersError return msg + + @override + async def publish_batch( + self, + cmd: "NatsPublishCommand", + ) -> None: + msg = "NATS doesn't support publishing in batches." + raise FeatureNotSupportedException(msg) + + +class FakeNatsFastProducer(ProducerProto): + def connect(self, connection: "Client") -> None: + raise NotImplementedError + + def disconnect(self) -> None: + raise NotImplementedError + + @override + async def publish( # type: ignore[override] + self, + cmd: "NatsPublishCommand", + ) -> None: + raise NotImplementedError + + @override + async def request( # type: ignore[override] + self, + cmd: "NatsPublishCommand", + ) -> "Msg": + raise NotImplementedError + + @override + async def publish_batch( + self, + cmd: "NatsPublishCommand", + ) -> None: + raise NotImplementedError diff --git a/faststream/nats/publisher/specification.py b/faststream/nats/publisher/specification.py new file mode 100644 index 0000000000..a5650e9670 --- /dev/null +++ b/faststream/nats/publisher/specification.py @@ -0,0 +1,42 @@ +from faststream._internal.endpoint.publisher import PublisherSpecification +from faststream.nats.configs import NatsBrokerConfig +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 .config import NatsPublisherSpecificationConfig + + +class NatsPublisherSpecification(PublisherSpecification[NatsBrokerConfig, NatsPublisherSpecificationConfig]): + @property + def subject(self) -> str: + return f"{self._outer_config.prefix}{self.config.subject}" + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{self.subject}:Publisher" + + def get_schema(self) -> dict[str, PublisherSpec]: + payloads = self.get_payloads() + + return { + self.name: PublisherSpec( + description=self.config.description_, + operation=Operation( + message=Message( + title=f"{self.name}:Message", + payload=resolve_payloads(payloads, "Publisher"), + ), + 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 eaa014ce75..9d0bfd4dfe 100644 --- a/faststream/nats/publisher/usecase.py +++ b/faststream/nats/publisher/usecase.py @@ -1,222 +1,190 @@ -from contextlib import AsyncExitStack -from functools import partial -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Dict, - Iterable, - Optional, - Sequence, - Union, -) +from collections.abc import Iterable +from typing import TYPE_CHECKING, Optional, Union from nats.aio.msg import Msg -from typing_extensions import Annotated, Doc, override +from typing_extensions import overload, override -from faststream.broker.message import SourceType, gen_cor_id -from faststream.broker.publisher.usecase import PublisherUsecase -from faststream.exceptions import NOT_CONNECTED_YET -from faststream.utils.functions import return_input +from faststream._internal.endpoint.publisher import PublisherUsecase +from faststream.message import gen_cor_id +from faststream.nats.response import NatsPublishCommand +from faststream.response.publish_type import PublishType if TYPE_CHECKING: - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware + from faststream._internal.basic_types import SendableMessage + from faststream._internal.types import PublisherMiddleware + from faststream.nats.configs import NatsBrokerConfig from faststream.nats.message import NatsMessage from faststream.nats.publisher.producer import NatsFastProducer, NatsJSFastProducer - from faststream.nats.schemas import JStream - from faststream.types import AnyDict, AsyncFunc, SendableMessage + from faststream.nats.schemas import PubAck + from faststream.response.response import PublishCommand + + from .config import NatsPublisherConfig class LogicPublisher(PublisherUsecase[Msg]): """A class to represent a NATS publisher.""" - _producer: Union["NatsFastProducer", "NatsJSFastProducer", None] + _outer_config: "NatsBrokerConfig" - def __init__( - self, - *, - subject: str, - reply_to: str, - headers: Optional[Dict[str, str]], - stream: Optional["JStream"], - timeout: Optional[float], - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[Msg]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: + def __init__(self, config: "NatsPublisherConfig", specification: "PublisherSpecification") -> 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, - ) + super().__init__(config, specification) + + self._subject = config.subject + self.stream = config.stream + self.timeout = config.timeout + self.headers = config.headers or {} + self.reply_to = config.reply_to - self.subject = subject - self.stream = stream - self.timeout = timeout - self.headers = headers - self.reply_to = reply_to + @property + def subject(self) -> str: + return f"{self._outer_config.prefix}{self._subject}" - def __hash__(self) -> int: - return hash(self.subject) + @overload + async def publish( + self, + message: "SendableMessage", + subject: str = "", + headers: dict[str, str] | None = None, + reply_to: str = "", + correlation_id: str | None = None, + stream: None = None, + timeout: float | None = None, + ) -> None: ... + + @overload + async def publish( + self, + message: "SendableMessage", + subject: str = "", + headers: dict[str, str] | None = None, + reply_to: str = "", + correlation_id: str | None = None, + stream: str | None = None, + timeout: float | None = None, + ) -> "PubAck": ... @override async def publish( self, message: "SendableMessage", subject: str = "", - *, - headers: Optional[Dict[str, str]] = None, + headers: dict[str, str] | None = None, reply_to: str = "", - correlation_id: Optional[str] = None, - stream: Optional[str] = None, - timeout: Optional[float] = None, - rpc: bool = False, - rpc_timeout: Optional[float] = 30.0, - raise_timeout: bool = False, - # publisher specific - _extra_middlewares: Iterable["PublisherMiddleware"] = (), - ) -> Optional[Any]: + correlation_id: str | None = None, + stream: str | None = None, + timeout: float | None = None, + ) -> Optional["PubAck"]: """Publish message directly. Args: - message (SendableMessage): Message body to send. + message: + Message body to send. Can be any encodable object (native python types or `pydantic.BaseModel`). - subject (str): NATS subject to send message (default is `''`). - headers (:obj:`dict` of :obj:`str`: :obj:`str`, optional): Message headers to store metainformation (default is `None`). + subject: + NATS subject to send message. + headers: + Message headers to store metainformation. **content-type** and **correlation_id** will be set automatically by framework anyway. - - reply_to (str): NATS subject name to send response (default is `None`). - correlation_id (str, optional): Manual message **correlation_id** setter (default is `None`). + reply_to: + NATS subject name to send response. + correlation_id: + Manual message **correlation_id** setter. **correlation_id** is a useful option to trace messages. + stream: + This option validates that the target subject is in presented stream. + Can be omitted without any effect if you doesn't want PubAck frame. + timeout: + Timeout to send message to NATS. + + Returns: + `None` if you publishes a regular message. + `faststream.nats.PubAck` if you publishes a message to stream. + """ + cmd = NatsPublishCommand( + message, + subject=subject or self.subject, + headers=self.headers | (headers or {}), + reply_to=reply_to or self.reply_to, + correlation_id=correlation_id or gen_cor_id(), + stream=stream or getattr(self.stream, "name", None), + timeout=timeout or self.timeout, + _publish_type=PublishType.PUBLISH, + ) + return await self._basic_publish(cmd, _extra_middlewares=()) - stream (str, optional): This option validates that the target subject is in presented stream (default is `None`). - Can be omitted without any effect. - timeout (float, optional): Timeout to send message to NATS in seconds (default is `None`). - rpc (bool): Whether to wait for reply in blocking mode (default is `False`). - rpc_timeout (float, optional): RPC reply waiting time (default is `30.0`). - raise_timeout (bool): Whetever to raise `TimeoutError` or return `None` at **rpc_timeout** (default is `False`). - RPC request returns `None` at timeout by default. + @property + def _producer(self) -> Union["NatsFastProducer", "NatsJSFastProducer"]: + if self.stream: + return self._outer_config.js_producer + return self._outer_config.producer - _extra_middlewares (:obj:`Iterable` of :obj:`PublisherMiddleware`): Extra middlewares to wrap publishing process (default is `()`). - """ - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - kwargs: AnyDict = { - "subject": subject or self.subject, - "headers": headers or self.headers, - "reply_to": reply_to or self.reply_to, - "correlation_id": correlation_id or gen_cor_id(), - # specific args - "rpc": rpc, - "rpc_timeout": rpc_timeout, - "raise_timeout": raise_timeout, - } - - if stream := stream or getattr(self.stream, "name", None): - kwargs.update({"stream": stream, "timeout": timeout or self.timeout}) - - call: AsyncFunc = self._producer.publish - - for m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - call = partial(m, call) - - return await call(message, **kwargs) + @override + async def _publish( + self, + cmd: Union["PublishCommand", "NatsPublishCommand"], + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> None: + """This method should be called in subscriber flow only.""" + cmd = NatsPublishCommand.from_cmd(cmd) + + cmd.destination = self.subject + cmd.add_headers(self.headers, override=False) + cmd.reply_to = cmd.reply_to or self.reply_to + + if self.stream: + cmd.stream = self.stream.name + cmd.timeout = self.timeout + + return await self._basic_publish(cmd, _extra_middlewares=_extra_middlewares) @override async def request( self, - message: Annotated[ - "SendableMessage", - Doc( - "Message body to send. " - "Can be any encodable object (native python types or `pydantic.BaseModel`)." - ), - ], - subject: Annotated[ - str, - Doc("NATS subject to send message."), - ] = "", - *, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway." - ), - ] = None, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - timeout: Annotated[ - float, - Doc("Timeout to send message to NATS."), - ] = 0.5, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), + message: "SendableMessage", + subject: str = "", + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + timeout: float = 0.5, ) -> "NatsMessage": - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - kwargs: AnyDict = { - "subject": subject or self.subject, - "headers": headers or self.headers, - "timeout": timeout or self.timeout, - "correlation_id": correlation_id or gen_cor_id(), - } - - request: AsyncFunc = self._producer.request - - for pub_m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - request = partial(pub_m, request) - - published_msg = await request( - message, - **kwargs, - ) + """Make a synchronous request to outer subscriber. - async with AsyncExitStack() as stack: - return_msg: Callable[[NatsMessage], Awaitable[NatsMessage]] = return_input - for m in self._broker_middlewares[::-1]: - mid = m(published_msg) - await stack.enter_async_context(mid) - return_msg = partial(mid.consume_scope, return_msg) + If out subscriber listens subject by stream, you should setup the same **stream** explicitly. + Another way you will reseave confirmation frame as a response. - parsed_msg = await self._producer._parser(published_msg) - parsed_msg._decoded_body = await self._producer._decoder(parsed_msg) - parsed_msg._source_type = SourceType.Response - return await return_msg(parsed_msg) + Note: + To setup **stream** option, please use `__init__` method. + + Args: + message: + Message body to send. + Can be any encodable object (native python types or `pydantic.BaseModel`). + subject: + NATS subject to send message. + headers: + Message headers to store metainformation. + **content-type** and **correlation_id** will be set automatically by framework anyway. + reply_to: + NATS subject name to send response. + correlation_id: + Manual message **correlation_id** setter. + **correlation_id** is a useful option to trace messages. + timeout: + Timeout to send message to NATS. - raise AssertionError("unreachable") + Returns: + `faststream.nats.message.NatsMessage` object as an outer subscriber response. + """ + cmd = NatsPublishCommand( + message=message, + subject=subject or self.subject, + headers=self.headers | (headers or {}), + timeout=timeout or self.timeout, + correlation_id=correlation_id or gen_cor_id(), + stream=getattr(self.stream, "name", None), + _publish_type=PublishType.REQUEST, + ) - def add_prefix(self, prefix: str) -> None: - self.subject = prefix + self.subject + msg: NatsMessage = await self._basic_request(cmd) + return msg diff --git a/faststream/nats/response.py b/faststream/nats/response.py index b3813131ff..fe8a310536 100644 --- a/faststream/nats/response.py +++ b/faststream/nats/response.py @@ -1,11 +1,12 @@ -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING, Union from typing_extensions import override -from faststream.broker.response import Response +from faststream.response.publish_type import PublishType +from faststream.response.response import PublishCommand, Response if TYPE_CHECKING: - from faststream.types import AnyDict, SendableMessage + from faststream._internal.basic_types import SendableMessage class NatsResponse(Response): @@ -13,9 +14,9 @@ def __init__( self, body: "SendableMessage", *, - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, - stream: Optional[str] = None, + headers: dict[str, str] | None = None, + correlation_id: str | None = None, + stream: str | None = None, ) -> None: super().__init__( body=body, @@ -25,9 +26,81 @@ def __init__( self.stream = stream @override - def as_publish_kwargs(self) -> "AnyDict": - publish_options = { - **super().as_publish_kwargs(), - "stream": self.stream, - } - return publish_options + def as_publish_command(self) -> "NatsPublishCommand": + return NatsPublishCommand( + message=self.body, + headers=self.headers, + correlation_id=self.correlation_id, + _publish_type=PublishType.PUBLISH, + # Nats specific + subject="", + stream=self.stream, + ) + + +class NatsPublishCommand(PublishCommand): + def __init__( + self, + message: "SendableMessage", + *, + subject: str = "", + correlation_id: str | None = None, + headers: dict[str, str] | None = None, + reply_to: str = "", + stream: str | None = None, + timeout: float | None = None, + _publish_type: PublishType, + ) -> None: + super().__init__( + body=message, + destination=subject, + correlation_id=correlation_id, + headers=headers, + reply_to=reply_to, + _publish_type=_publish_type, + ) + + self.stream = stream + self.timeout = timeout + + def headers_to_publish(self, *, js: bool = False) -> dict[str, str]: + headers = {} + + if self.correlation_id: + headers["correlation_id"] = self.correlation_id + + if js and self.reply_to: + headers["reply_to"] = self.reply_to + + return headers | self.headers + + @classmethod + def from_cmd( + cls, + cmd: Union["PublishCommand", "NatsPublishCommand"], + ) -> "NatsPublishCommand": + if isinstance(cmd, NatsPublishCommand): + # NOTE: Should return a copy probably. + return cmd + + return cls( + message=cmd.body, + subject=cmd.destination, + correlation_id=cmd.correlation_id, + headers=cmd.headers, + reply_to=cmd.reply_to, + _publish_type=cmd.publish_type, + ) + + def __repr__(self) -> str: + body = [f"body='{self.body}'", f"subject='{self.destination}'"] + if self.stream: + body.append(f"stream={self.stream}") + if self.reply_to: + body.append(f"reply_to='{self.reply_to}'") + body.extend(( + f"headers={self.headers}", + f"correlation_id='{self.correlation_id}'", + f"publish_type={self.publish_type}", + )) + return f"{self.__class__.__name__}({', '.join(body)})" diff --git a/faststream/nats/router.py b/faststream/nats/router.py deleted file mode 100644 index b9e029c594..0000000000 --- a/faststream/nats/router.py +++ /dev/null @@ -1,383 +0,0 @@ -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Dict, - Iterable, - Optional, - Sequence, - Union, -) - -from nats.js import api -from typing_extensions import Annotated, Doc, deprecated - -from faststream.broker.router import ArgsContainer, BrokerRouter, SubscriberRoute -from faststream.broker.utils import default_filter -from faststream.nats.broker.registrator import NatsRegistrator - -if TYPE_CHECKING: - from fast_depends.dependencies import Depends - from nats.aio.msg import Msg - - from faststream.broker.types import ( - BrokerMiddleware, - CustomCallable, - Filter, - PublisherMiddleware, - SubscriberMiddleware, - ) - from faststream.nats.message import NatsBatchMessage, NatsMessage - from faststream.nats.schemas import JStream, KvWatch, ObjWatch, PullSub - from faststream.types import SendableMessage - - -class NatsPublisher(ArgsContainer): - """Delayed NatsPublisher registration object. - - Just a copy of `KafkaRegistrator.publisher(...)` arguments. - """ - - def __init__( - self, - subject: Annotated[ - str, - Doc("NATS subject to send message."), - ], - *, - headers: Annotated[ - Optional[Dict[str, str]], - Doc( - "Message headers to store metainformation. " - "**content-type** and **correlation_id** will be set automatically by framework anyway. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("NATS subject name to send response."), - ] = "", - # JS - stream: Annotated[ - Union[str, "JStream", None], - Doc( - "This option validates that the target `subject` is in presented stream. " - "Can be omitted without any effect." - ), - ] = None, - timeout: Annotated[ - Optional[float], - Doc("Timeout to send message to NATS."), - ] = None, - # basic args - middlewares: Annotated[ - Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI information - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - ) -> None: - super().__init__( - subject=subject, - headers=headers, - reply_to=reply_to, - stream=stream, - timeout=timeout, - middlewares=middlewares, - title=title, - description=description, - schema=schema, - include_in_schema=include_in_schema, - ) - - -class NatsRoute(SubscriberRoute): - """Class to store delayed NatsBroker subscriber registration.""" - - def __init__( - self, - call: Annotated[ - Union[ - Callable[..., "SendableMessage"], - Callable[..., Awaitable["SendableMessage"]], - ], - Doc( - "Message handler function " - "to wrap the same with `@broker.subscriber(...)` way." - ), - ], - subject: Annotated[ - str, - Doc("NATS subject to subscribe."), - ], - publishers: Annotated[ - Iterable[NatsPublisher], - Doc("Nats publishers to broadcast the handler result."), - ] = (), - queue: Annotated[ - str, - Doc( - "Subscribers' NATS queue name. Subscribers with same queue name will be load balanced by the NATS " - "server." - ), - ] = "", - pending_msgs_limit: Annotated[ - Optional[int], - Doc( - "Limit of messages, considered by NATS server as possible to be delivered to the client without " - "been answered. In case of NATS Core, if that limits exceeds, you will receive NATS 'Slow Consumer' " - "error. " - "That's literally means that your worker can't handle the whole load. In case of NATS JetStream, " - "you will no longer receive messages until some of delivered messages will be acked in any way." - ), - ] = None, - pending_bytes_limit: Annotated[ - Optional[int], - Doc( - "The number of bytes, considered by NATS server as possible to be delivered to the client without " - "been answered. In case of NATS Core, if that limit exceeds, you will receive NATS 'Slow Consumer' " - "error." - "That's literally means that your worker can't handle the whole load. In case of NATS JetStream, " - "you will no longer receive messages until some of delivered messages will be acked in any way." - ), - ] = None, - # Core arguments - max_msgs: Annotated[ - int, - Doc("Consuming messages limiter. Automatically disconnect if reached."), - ] = 0, - # JS arguments - durable: Annotated[ - Optional[str], - Doc( - "Name of the durable consumer to which the the subscription should be bound." - ), - ] = None, - config: Annotated[ - Optional["api.ConsumerConfig"], - Doc("Configuration of JetStream consumer to be subscribed with."), - ] = None, - ordered_consumer: Annotated[ - bool, - Doc("Enable ordered consumer mode."), - ] = False, - idle_heartbeat: Annotated[ - Optional[float], - Doc("Enable Heartbeats for a consumer to detect failures."), - ] = None, - flow_control: Annotated[ - Optional[bool], - Doc("Enable Flow Control for a consumer."), - ] = None, - deliver_policy: Annotated[ - Optional["api.DeliverPolicy"], - Doc("Deliver Policy to be used for subscription."), - ] = None, - headers_only: Annotated[ - Optional[bool], - Doc( - "Should be message delivered without payload, only headers and metadata." - ), - ] = None, - # pull arguments - pull_sub: Annotated[ - Optional["PullSub"], - Doc( - "NATS Pull consumer parameters container. " - "Should be used with `stream` only." - ), - ] = None, - kv_watch: Annotated[ - Union[str, "KvWatch", None], - Doc("KeyValue watch parameters container."), - ] = None, - obj_watch: Annotated[ - Union[bool, "ObjWatch"], - Doc("ObjecStore watch parameters container."), - ] = False, - inbox_prefix: Annotated[ - bytes, - Doc( - "Prefix for generating unique inboxes, subjects with that prefix and NUID." - ), - ] = api.INBOX_PREFIX, - # custom - ack_first: Annotated[ - bool, - Doc("Whether to `ack` message at start of consuming or not."), - ] = False, - stream: Annotated[ - Union[str, "JStream", None], - Doc("Subscribe to NATS Stream with `subject` filter."), - ] = None, - # broker arguments - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **nats-py** Msg to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - middlewares: Annotated[ - Sequence["SubscriberMiddleware[NatsMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - Union[ - "Filter[NatsMessage]", - "Filter[NatsBatchMessage]", - ], - Doc( - "Overload subscriber to consume various messages from the same source." - ), - deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - max_workers: Annotated[ - int, - Doc("Number of workers to process messages concurrently."), - ] = 1, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI information - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - ) -> None: - super().__init__( - call, - subject=subject, - publishers=publishers, - pending_msgs_limit=pending_msgs_limit, - pending_bytes_limit=pending_bytes_limit, - max_msgs=max_msgs, - durable=durable, - config=config, - ordered_consumer=ordered_consumer, - idle_heartbeat=idle_heartbeat, - flow_control=flow_control, - deliver_policy=deliver_policy, - headers_only=headers_only, - pull_sub=pull_sub, - kv_watch=kv_watch, - obj_watch=obj_watch, - inbox_prefix=inbox_prefix, - ack_first=ack_first, - stream=stream, - max_workers=max_workers, - queue=queue, - dependencies=dependencies, - parser=parser, - decoder=decoder, - middlewares=middlewares, - filter=filter, - retry=retry, - no_ack=no_ack, - no_reply=no_reply, - title=title, - description=description, - include_in_schema=include_in_schema, - ) - - -class NatsRouter( - NatsRegistrator, - BrokerRouter["Msg"], -): - """Includable to NatsBroker router.""" - - def __init__( - self, - prefix: Annotated[ - str, - Doc("String prefix to add to all subscribers subjects."), - ] = "", - handlers: Annotated[ - Iterable[NatsRoute], - Doc("Route object to include."), - ] = (), - *, - dependencies: Annotated[ - Iterable["Depends"], - Doc( - "Dependencies list (`[Depends(),]`) to apply to all routers' publishers/subscribers." - ), - ] = (), - middlewares: Annotated[ - Sequence["BrokerMiddleware[Msg]"], - Doc("Router middlewares to apply to all routers' publishers/subscribers."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **IncomingMessage** Msg to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - include_in_schema: Annotated[ - Optional[bool], - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = None, - ) -> None: - super().__init__( - handlers=handlers, - # basic args - prefix=prefix, - dependencies=dependencies, - middlewares=middlewares, - parser=parser, - decoder=decoder, - include_in_schema=include_in_schema, - ) diff --git a/faststream/nats/schemas/__init__.py b/faststream/nats/schemas/__init__.py index 1edd51bcbe..accadfc731 100644 --- a/faststream/nats/schemas/__init__.py +++ b/faststream/nats/schemas/__init__.py @@ -1,3 +1,5 @@ +from nats.js.api import PubAck + from faststream.nats.schemas.js_stream import JStream from faststream.nats.schemas.kv_watch import KvWatch from faststream.nats.schemas.obj_watch import ObjWatch @@ -7,5 +9,6 @@ "JStream", "KvWatch", "ObjWatch", + "PubAck", "PullSub", ) diff --git a/faststream/nats/schemas/js_stream.py b/faststream/nats/schemas/js_stream.py index 017691da23..89509ca571 100644 --- a/faststream/nats/schemas/js_stream.py +++ b/faststream/nats/schemas/js_stream.py @@ -1,11 +1,11 @@ from itertools import zip_longest -from typing import TYPE_CHECKING, List, Optional, Tuple +from typing import TYPE_CHECKING, Annotated, Optional from nats.js.api import DiscardPolicy, StreamConfig -from typing_extensions import Annotated, Doc +from typing_extensions import Doc -from faststream.broker.schemas import NameRequired -from faststream.utils.path import compile_path +from faststream._internal.proto import NameRequired +from faststream._internal.utils.path import compile_path if TYPE_CHECKING: from re import Pattern @@ -36,16 +36,16 @@ def __init__( Doc("Stream name to work with."), ], description: Annotated[ - Optional[str], + str | None, Doc("Stream description if needed."), ] = None, subjects: Annotated[ - Optional[List[str]], + list[str] | None, Doc( "Subjects, used by stream to grab messages from them. Any message sent by NATS Core will be consumed " "by stream. Also, stream acknowledge message publisher with message, sent on reply subject of " "publisher. Can be single string or list of them. Dots separate tokens of subjects, every token may " - "be matched with exact same token or wildcards." + "be matched with exact same token or wildcards.", ), ] = None, retention: Annotated[ @@ -60,24 +60,25 @@ def __init__( "which guarantees message to be consumed only once. Since message acked, it will be deleted from the " "stream immediately. Note: Message will be deleted only if limit is reached or message acked " "successfully. Message that reached MaxDelivery limit will remain in the stream and should be " - "manually deleted! Note: All policies will be responsive to Limits." + "manually deleted! Note: All policies will be responsive to Limits.", ), ] = None, max_consumers: Annotated[ - Optional[int], Doc("Max number of consumers to be bound with this stream.") + int | None, + Doc("Max number of consumers to be bound with this stream."), ] = None, max_msgs: Annotated[ - Optional[int], + int | None, Doc( "Max number of messages to be stored in the stream. Stream can automatically delete old messages or " - "stop receiving new messages, look for 'DiscardPolicy'" + "stop receiving new messages, look for 'DiscardPolicy'", ), ] = None, max_bytes: Annotated[ - Optional[int], + int | None, Doc( "Max bytes of all messages to be stored in the stream. Stream can automatically delete old messages or " - "stop receiving new messages, look for 'DiscardPolicy'" + "stop receiving new messages, look for 'DiscardPolicy'", ), ] = None, discard: Annotated[ @@ -85,38 +86,38 @@ def __init__( Doc("Determines stream behavior on messages in case of retention exceeds."), ] = DiscardPolicy.OLD, max_age: Annotated[ - Optional[float], + float | None, Doc( "TTL in seconds for messages. Since message arrive, TTL begun. As soon as TTL exceeds, message will be " - "deleted." + "deleted.", ), ] = None, # in seconds max_msgs_per_subject: Annotated[ int, Doc( - "Limit message count per every unique subject. Stream index subjects to it's pretty fast tho.-" + "Limit message count per every unique subject. Stream index subjects to it's pretty fast tho.-", ), ] = -1, max_msg_size: Annotated[ - Optional[int], + int | None, Doc( "Limit message size to be received. Note: the whole message can't be larger than NATS Core message " - "limit." + "limit.", ), ] = -1, storage: Annotated[ Optional["StorageType"], Doc( "Storage type, disk or memory. Disk is more durable, memory is faster. Memory can be better choice " - "for systems, where new value overrides previous." + "for systems, where new value overrides previous.", ), ] = None, num_replicas: Annotated[ - Optional[int], + int | None, Doc( "Replicas of stream to be used. All replicas create RAFT group with leader. In case of losing lesser " "than half, cluster will be available to reads and writes. In case of losing slightly more than half, " - "cluster may be available but for reads only." + "cluster may be available but for reads only.", ), ] = None, no_ack: Annotated[ @@ -126,31 +127,31 @@ def __init__( "received by stream or not." ), ] = False, - template_owner: Optional[str] = None, + template_owner: str | None = None, duplicate_window: Annotated[ float, Doc( "A TTL for keys in implicit TTL-based hashmap of stream. That hashmap allows to early drop duplicate " "messages. Essential feature for idempotent writes. Note: disabled by default. Look for 'Nats-Msg-Id' " - "in NATS documentation for more information." + "in NATS documentation for more information.", ), ] = 0, placement: Annotated[ Optional["Placement"], Doc( - "NATS Cluster for stream to be deployed in. Value is name of that cluster." + "NATS Cluster for stream to be deployed in. Value is name of that cluster.", ), ] = None, mirror: Annotated[ Optional["StreamSource"], Doc( - "Should stream be read-only replica of another stream, if so, value is name of that stream." + "Should stream be read-only replica of another stream, if so, value is name of that stream.", ), ] = None, sources: Annotated[ - Optional[List["StreamSource"]], + list["StreamSource"] | None, Doc( - "Should stream mux multiple streams into single one, if so, values is names of those streams." + "Should stream mux multiple streams into single one, if so, values is names of those streams.", ), ] = None, sealed: Annotated[ @@ -174,11 +175,11 @@ def __init__( Doc("Should be messages, received by stream, send to additional subject."), ] = None, allow_direct: Annotated[ - Optional[bool], + bool | None, Doc("Should direct requests be allowed. Note: you can get stale data."), ] = None, mirror_direct: Annotated[ - Optional[bool], + bool | None, Doc("Should direct mirror requests be allowed"), ] = None, # custom @@ -193,6 +194,7 @@ def __init__( self.subjects = subjects self.declare = declare + self.config = StreamConfig( name=name, description=description, @@ -245,14 +247,14 @@ def is_subject_match_wildcard(subject: str, wildcard: str) -> bool: if base == ">": break - if base != "*" and current != base: + if base not in {"*", current}: call = False break return call -def compile_nats_wildcard(pattern: str) -> Tuple[Optional["Pattern[str]"], str]: +def compile_nats_wildcard(pattern: str) -> tuple[Optional["Pattern[str]"], str]: return compile_path( pattern, replace_symbol="*", diff --git a/faststream/nats/schemas/kv_watch.py b/faststream/nats/schemas/kv_watch.py index e99a5f5084..4bb0afcd3b 100644 --- a/faststream/nats/schemas/kv_watch.py +++ b/faststream/nats/schemas/kv_watch.py @@ -1,6 +1,5 @@ -from typing import Optional -from faststream.broker.schemas import NameRequired +from faststream._internal.proto import NameRequired class KvWatch(NameRequired): @@ -35,8 +34,8 @@ def __init__( include_history: bool = False, ignore_deletes: bool = False, meta_only: bool = False, - inactive_threshold: Optional[float] = None, - timeout: Optional[float] = 5.0, + inactive_threshold: float | None = None, + timeout: float | None = 5.0, # custom declare: bool = True, ) -> None: @@ -50,6 +49,3 @@ def __init__( self.timeout = timeout self.declare = declare - - def __hash__(self) -> int: - return hash(self.name) diff --git a/faststream/nats/schemas/obj_watch.py b/faststream/nats/schemas/obj_watch.py index a1f11d4667..c8a3b9f245 100644 --- a/faststream/nats/schemas/obj_watch.py +++ b/faststream/nats/schemas/obj_watch.py @@ -56,7 +56,6 @@ def validate(cls, value: Union[bool, "ObjWatch"]) -> Optional["ObjWatch"]: ... def validate(cls, value: Union[bool, "ObjWatch"]) -> Optional["ObjWatch"]: if value is True: return ObjWatch() - elif value is False: + if value is False: return None - else: - return value + return value diff --git a/faststream/nats/schemas/pull_sub.py b/faststream/nats/schemas/pull_sub.py index b38b48ebdb..175a858c23 100644 --- a/faststream/nats/schemas/pull_sub.py +++ b/faststream/nats/schemas/pull_sub.py @@ -20,7 +20,7 @@ class PullSub: def __init__( self, batch_size: int = 1, - timeout: Optional[float] = 5.0, + timeout: float | None = 5.0, batch: bool = False, ) -> None: self.batch_size = batch_size @@ -47,7 +47,6 @@ def validate(cls, value: Union[bool, "PullSub"]) -> Optional["PullSub"]: ... def validate(cls, value: Union[bool, "PullSub"]) -> Optional["PullSub"]: if value is True: return PullSub() - elif value is False: + if value is False: return None - else: - return value + return value diff --git a/faststream/nats/security.py b/faststream/nats/security.py index c80931055b..c0a7e1e09a 100644 --- a/faststream/nats/security.py +++ b/faststream/nats/security.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from faststream.security import ( BaseSecurity, @@ -6,18 +6,18 @@ ) if TYPE_CHECKING: - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict -def parse_security(security: Optional[BaseSecurity]) -> "AnyDict": +def parse_security(security: BaseSecurity | None) -> "AnyDict": if security is None: return {} - elif isinstance(security, SASLPlaintext): + if isinstance(security, SASLPlaintext): return _parse_sasl_plaintext(security) - elif isinstance(security, BaseSecurity): + if isinstance(security, BaseSecurity): return _parse_base_security(security) - else: - raise NotImplementedError(f"NatsBroker does not support {type(security)}") + msg = f"NatsBroker does not support {type(security)}" + raise NotImplementedError(msg) def _parse_base_security(security: BaseSecurity) -> "AnyDict": diff --git a/faststream/nats/subscriber/adapters.py b/faststream/nats/subscriber/adapters.py new file mode 100644 index 0000000000..7051946521 --- /dev/null +++ b/faststream/nats/subscriber/adapters.py @@ -0,0 +1,26 @@ +from typing import Any, Generic, Protocol, TypeVar + + +class Unsubscriptable(Protocol): + async def unsubscribe(self) -> None: ... + + +class Watchable(Protocol): + async def stop(self) -> None: ... + + async def updates(self, timeout: float) -> Any | None: ... + + +WatchableT = TypeVar("WatchableT", bound=Watchable) + + +class UnsubscribeAdapter(Unsubscriptable, Generic[WatchableT]): + __slots__ = ("obj",) + + obj: WatchableT + + def __init__(self, subscription: WatchableT) -> None: + self.obj = subscription + + async def unsubscribe(self) -> None: + await self.obj.stop() diff --git a/faststream/nats/subscriber/asyncapi.py b/faststream/nats/subscriber/asyncapi.py deleted file mode 100644 index 402aa0b114..0000000000 --- a/faststream/nats/subscriber/asyncapi.py +++ /dev/null @@ -1,112 +0,0 @@ -from typing import Any, Dict - -from typing_extensions import override - -from faststream.asyncapi.schema import ( - Channel, - ChannelBinding, - CorrelationId, - Message, - Operation, -) -from faststream.asyncapi.schema.bindings import nats -from faststream.asyncapi.utils import resolve_payloads -from faststream.nats.subscriber.usecase import ( - BatchPullStreamSubscriber, - ConcurrentCoreSubscriber, - ConcurrentPullStreamSubscriber, - ConcurrentPushStreamSubscriber, - CoreSubscriber, - KeyValueWatchSubscriber, - LogicSubscriber, - ObjStoreWatchSubscriber, - PullStreamSubscriber, - PushStreamSubscription, -) - - -class AsyncAPISubscriber(LogicSubscriber[Any, Any]): - """A class to represent a NATS handler.""" - - def get_name(self) -> str: - return f"{self.subject}:{self.call_name}" - - def get_schema(self) -> Dict[str, Channel]: - payloads = self.get_payloads() - - return { - self.name: Channel( - description=self.description, - subscribe=Operation( - message=Message( - title=f"{self.name}:Message", - payload=resolve_payloads(payloads), - correlationId=CorrelationId( - location="$message.header#/correlation_id" - ), - ), - ), - bindings=ChannelBinding( - nats=nats.ChannelBinding( - subject=self.subject, - queue=getattr(self, "queue", "") or None, - ) - ), - ) - } - - -class AsyncAPICoreSubscriber(AsyncAPISubscriber, CoreSubscriber): - """One-message core consumer with AsyncAPI methods.""" - - -class AsyncAPIConcurrentCoreSubscriber(AsyncAPISubscriber, ConcurrentCoreSubscriber): - """One-message core concurrent consumer with AsyncAPI methods.""" - - -class AsyncAPIStreamSubscriber(AsyncAPISubscriber, PushStreamSubscription): - """One-message JS Push consumer with AsyncAPI methods.""" - - -class AsyncAPIConcurrentPushStreamSubscriber( - AsyncAPISubscriber, ConcurrentPushStreamSubscriber -): - """One-message JS Push concurrent consumer with AsyncAPI methods.""" - - -class AsyncAPIPullStreamSubscriber(AsyncAPISubscriber, PullStreamSubscriber): - """One-message JS Pull consumer with AsyncAPI methods.""" - - -class AsyncAPIConcurrentPullStreamSubscriber( - AsyncAPISubscriber, ConcurrentPullStreamSubscriber -): - """One-message JS Pull concurrent consumer with AsyncAPI methods.""" - - -class AsyncAPIBatchPullStreamSubscriber(AsyncAPISubscriber, BatchPullStreamSubscriber): - """Batch-message Pull consumer with AsyncAPI methods.""" - - -class AsyncAPIKeyValueWatchSubscriber(AsyncAPISubscriber, KeyValueWatchSubscriber): - """KeyValueWatch consumer with AsyncAPI methods.""" - - @override - def get_name(self) -> str: - return "" - - @override - def get_schema(self) -> Dict[str, Channel]: - return {} - - -class AsyncAPIObjStoreWatchSubscriber(AsyncAPISubscriber, ObjStoreWatchSubscriber): - """ObjStoreWatch consumer with AsyncAPI methods.""" - - @override - def get_name(self) -> str: - return "" - - @override - def get_schema(self) -> Dict[str, Channel]: - return {} diff --git a/faststream/nats/subscriber/config.py b/faststream/nats/subscriber/config.py new file mode 100644 index 0000000000..7eb1459d9e --- /dev/null +++ b/faststream/nats/subscriber/config.py @@ -0,0 +1,46 @@ +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Optional + +from faststream._internal.configs import ( + SubscriberSpecificationConfig, + SubscriberUsecaseConfig, +) +from faststream._internal.constants import EMPTY +from faststream.middlewares import AckPolicy +from faststream.nats.configs import NatsBrokerConfig + +if TYPE_CHECKING: + from nats.js.api import ConsumerConfig + + from faststream._internal.basic_types import AnyDict + + +@dataclass(kw_only=True) +class NatsSubscriberSpecificationConfig(SubscriberSpecificationConfig): + subject: str + queue: str | None + + +@dataclass(kw_only=True) +class NatsSubscriberConfig(SubscriberUsecaseConfig): + _outer_config: "NatsBrokerConfig" = field(default_factory=NatsBrokerConfig) + + subject: str + sub_config: "ConsumerConfig" + extra_options: Optional["AnyDict"] = field(default_factory=dict) + + _ack_first: bool = field(default_factory=lambda: EMPTY, repr=False) + _no_ack: bool = field(default_factory=lambda: EMPTY, repr=False) + + @property + def ack_policy(self) -> AckPolicy: + if self._no_ack is not EMPTY and self._no_ack: + return AckPolicy.DO_NOTHING + + if self._ack_first is not EMPTY and self._ack_first: + return AckPolicy.ACK_FIRST + + if self._ack_policy is EMPTY: + return AckPolicy.REJECT_ON_ERROR + + return self._ack_policy diff --git a/faststream/nats/subscriber/factory.py b/faststream/nats/subscriber/factory.py index 4d42041743..1aa474f3df 100644 --- a/faststream/nats/subscriber/factory.py +++ b/faststream/nats/subscriber/factory.py @@ -1,5 +1,5 @@ import warnings -from typing import TYPE_CHECKING, Any, Iterable, Optional, Sequence, Union +from typing import TYPE_CHECKING, Optional from nats.aio.subscription import ( DEFAULT_SUB_PENDING_BYTES_LIMIT, @@ -11,44 +11,49 @@ DEFAULT_JS_SUB_PENDING_MSGS_LIMIT, ) +from faststream._internal.constants import EMPTY +from faststream._internal.endpoint.subscriber.call_item import CallsCollection from faststream.exceptions import SetupError -from faststream.nats.subscriber.asyncapi import ( - AsyncAPIBatchPullStreamSubscriber, - AsyncAPIConcurrentCoreSubscriber, - AsyncAPIConcurrentPullStreamSubscriber, - AsyncAPIConcurrentPushStreamSubscriber, - AsyncAPICoreSubscriber, - AsyncAPIKeyValueWatchSubscriber, - AsyncAPIObjStoreWatchSubscriber, - AsyncAPIPullStreamSubscriber, - AsyncAPIStreamSubscriber, +from faststream.middlewares import AckPolicy + +from .config import NatsSubscriberConfig, NatsSubscriberSpecificationConfig +from .specification import NatsSubscriberSpecification, NotIncludeSpecifation +from .usecases import ( + BatchPullStreamSubscriber, + ConcurrentCoreSubscriber, + ConcurrentPullStreamSubscriber, + ConcurrentPushStreamSubscriber, + CoreSubscriber, + KeyValueWatchSubscriber, + ObjStoreWatchSubscriber, + PullStreamSubscriber, + PushStreamSubscriber, ) if TYPE_CHECKING: - from fast_depends.dependencies import Depends from nats.js import api - from faststream.broker.types import BrokerMiddleware + from faststream._internal.basic_types import AnyDict + from faststream.nats.configs import NatsBrokerConfig from faststream.nats.schemas import JStream, KvWatch, ObjWatch, PullSub - from faststream.types import AnyDict def create_subscriber( *, subject: str, queue: str, - pending_msgs_limit: Optional[int], - pending_bytes_limit: Optional[int], + pending_msgs_limit: int | None, + pending_bytes_limit: int | None, # Core args max_msgs: int, # JS args - durable: Optional[str], + durable: str | None, config: Optional["api.ConsumerConfig"], ordered_consumer: bool, - idle_heartbeat: Optional[float], - flow_control: Optional[bool], + idle_heartbeat: float | None, + flow_control: bool | None, deliver_policy: Optional["api.DeliverPolicy"], - headers_only: Optional[bool], + headers_only: bool | None, # pull args pull_sub: Optional["PullSub"], kv_watch: Optional["KvWatch"], @@ -59,26 +64,15 @@ def create_subscriber( max_workers: int, stream: Optional["JStream"], # Subscriber args + ack_policy: "AckPolicy", no_ack: bool, no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[Any]"], - # AsyncAPI information - title_: Optional[str], - description_: Optional[str], + broker_config: "NatsBrokerConfig", + # Specification information + title_: str | None, + description_: str | None, include_in_schema: bool, -) -> Union[ - "AsyncAPICoreSubscriber", - "AsyncAPIConcurrentCoreSubscriber", - "AsyncAPIStreamSubscriber", - "AsyncAPIConcurrentPushStreamSubscriber", - "AsyncAPIPullStreamSubscriber", - "AsyncAPIConcurrentPullStreamSubscriber", - "AsyncAPIBatchPullStreamSubscriber", - "AsyncAPIKeyValueWatchSubscriber", - "AsyncAPIObjStoreWatchSubscriber", -]: +) -> BatchPullStreamSubscriber | ConcurrentCoreSubscriber | ConcurrentPullStreamSubscriber | ConcurrentPushStreamSubscriber | CoreSubscriber | KeyValueWatchSubscriber | ObjStoreWatchSubscriber | PullStreamSubscriber | PushStreamSubscriber: _validate_input_for_misconfigure( subject=subject, queue=queue, @@ -93,6 +87,8 @@ def create_subscriber( deliver_policy=deliver_policy, headers_only=headers_only, pull_sub=pull_sub, + ack_policy=ack_policy, + no_ack=no_ack, kv_watch=kv_watch, obj_watch=obj_watch, ack_first=ack_first, @@ -127,6 +123,12 @@ def create_subscriber( else: # JS Push Subscriber + if ack_first or ack_policy is AckPolicy.ACK_FIRST: + manual_ack = False + ack_policy = AckPolicy.DO_NOTHING + else: + manual_ack = True + extra_options.update( { "ordered_consumer": ordered_consumer, @@ -134,8 +136,8 @@ def create_subscriber( "flow_control": flow_control, "deliver_policy": deliver_policy, "headers_only": headers_only, - "manual_ack": not ack_first, - } + "manual_ack": manual_ack, + }, ) else: @@ -147,226 +149,210 @@ def create_subscriber( "max_msgs": max_msgs, } + subscriber_config = NatsSubscriberConfig( + subject=subject, + sub_config=config, + extra_options=extra_options, + no_reply=no_reply, + _outer_config=broker_config, + _ack_first=ack_first, + _ack_policy=ack_policy, + _no_ack=no_ack, + ) + + calls = CallsCollection() + + specification_config = NatsSubscriberSpecificationConfig( + subject=subject, + queue=queue or None, + title_=title_, + description_=description_, + include_in_schema=include_in_schema, + ) + + specification = NatsSubscriberSpecification( + _outer_config=broker_config, + calls=calls, + specification_config=specification_config, + ) + + not_include_spec = NotIncludeSpecifation( + _outer_config=broker_config, + calls=calls, + specification_config=specification_config, + ) + + subscriber_options = { + "config": subscriber_config, + "specification": specification, + "calls": calls, + } + if obj_watch is not None: - return AsyncAPIObjStoreWatchSubscriber( - subject=subject, - config=config, + return ObjStoreWatchSubscriber( + **(subscriber_options | {"specification": not_include_spec}), obj_watch=obj_watch, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) if kv_watch is not None: - return AsyncAPIKeyValueWatchSubscriber( - subject=subject, - config=config, + return KeyValueWatchSubscriber( + **(subscriber_options | {"specification": not_include_spec}), kv_watch=kv_watch, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) - elif stream is None: + if stream is None: if max_workers > 1: - return AsyncAPIConcurrentCoreSubscriber( + return ConcurrentCoreSubscriber( + **subscriber_options, max_workers=max_workers, - subject=subject, - config=config, queue=queue, - # basic args - extra_options=extra_options, - # Subscriber args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI information - title_=title_, - description_=description_, - include_in_schema=include_in_schema, ) - else: - return AsyncAPICoreSubscriber( - subject=subject, - config=config, + return CoreSubscriber( + **subscriber_options, + queue=queue, + ) + + if max_workers > 1: + if pull_sub is not None: + return ConcurrentPullStreamSubscriber( + **subscriber_options, queue=queue, - # basic args - extra_options=extra_options, - # Subscriber args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI information - title_=title_, - description_=description_, - include_in_schema=include_in_schema, + max_workers=max_workers, + pull_sub=pull_sub, ) - else: - if max_workers > 1: - if pull_sub is not None: - return AsyncAPIConcurrentPullStreamSubscriber( - max_workers=max_workers, - pull_sub=pull_sub, - stream=stream, - subject=subject, - config=config, - # basic args - extra_options=extra_options, - # Subscriber args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI information - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + return ConcurrentPushStreamSubscriber( + **subscriber_options, + max_workers=max_workers, + ) - else: - return AsyncAPIConcurrentPushStreamSubscriber( - max_workers=max_workers, - stream=stream, - subject=subject, - config=config, - queue=queue, - # basic args - extra_options=extra_options, - # Subscriber args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI information - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + if pull_sub is not None: + if pull_sub.batch: + return BatchPullStreamSubscriber( + **subscriber_options, + pull_sub=pull_sub, + stream=stream, + ) - else: - if pull_sub is not None: - if pull_sub.batch: - return AsyncAPIBatchPullStreamSubscriber( - pull_sub=pull_sub, - stream=stream, - subject=subject, - config=config, - # basic args - extra_options=extra_options, - # Subscriber args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI information - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - else: - return AsyncAPIPullStreamSubscriber( - pull_sub=pull_sub, - stream=stream, - subject=subject, - config=config, - # basic args - extra_options=extra_options, - # Subscriber args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI information - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + return PullStreamSubscriber( + **subscriber_options, + queue=queue, + pull_sub=pull_sub, + stream=stream, + ) - else: - return AsyncAPIStreamSubscriber( - stream=stream, - subject=subject, - queue=queue, - config=config, - # basic args - extra_options=extra_options, - # Subscriber args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI information - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) + return PushStreamSubscriber( + **subscriber_options, + queue=queue, + stream=stream, + ) -def _validate_input_for_misconfigure( +def _validate_input_for_misconfigure( # noqa: PLR0915 subject: str, queue: str, # default "" - pending_msgs_limit: Optional[int], - pending_bytes_limit: Optional[int], + pending_msgs_limit: int | None, + pending_bytes_limit: int | None, max_msgs: int, # default 0 - durable: Optional[str], + durable: str | None, config: Optional["api.ConsumerConfig"], ordered_consumer: bool, # default False - idle_heartbeat: Optional[float], - flow_control: Optional[bool], + idle_heartbeat: float | None, + flow_control: bool | None, deliver_policy: Optional["api.DeliverPolicy"], - headers_only: Optional[bool], + headers_only: bool | None, pull_sub: Optional["PullSub"], kv_watch: Optional["KvWatch"], obj_watch: Optional["ObjWatch"], - ack_first: bool, # default False + ack_policy: "AckPolicy", # default EMPTY + no_ack: bool, # default EMPTY + ack_first: bool, # default EMPTY max_workers: int, # default 1 stream: Optional["JStream"], ) -> None: - if not subject and not config: - raise SetupError("You must provide either the `subject` or `config` option.") + if ack_policy is not EMPTY: + if obj_watch is not None: + warnings.warn( + "You can't use acknowledgement policy with ObjectStorage watch subscriber.", + RuntimeWarning, + stacklevel=4, + ) - if stream and kv_watch: - raise SetupError( - "You can't use both the `stream` and `kv_watch` options simultaneously." + elif kv_watch is not None: + warnings.warn( + "You can't use acknowledgement policy with KeyValue watch subscriber.", + RuntimeWarning, + stacklevel=4, + ) + + elif stream is None and ack_policy is not AckPolicy.DO_NOTHING: + warnings.warn( + ( + "Core subscriber supports only `ack_policy=AckPolicy.DO_NOTHING` option for very specific cases. " + "If you are using different option, probably, you should use JetStream Subscriber instead." + ), + RuntimeWarning, + stacklevel=4, + ) + + if max_msgs > 0 and any((stream, kv_watch, obj_watch)): + warnings.warn( + "The `max_msgs` option can be used only with a NATS Core Subscriber.", + RuntimeWarning, + stacklevel=4, + ) + + if ack_first is not EMPTY: + warnings.warn( + "`ack_first` option was deprecated in prior to `ack_policy=AckPolicy.ACK_FIRST`. Scheduled to remove in 0.7.0", + category=DeprecationWarning, + stacklevel=4, ) - if stream and obj_watch: - raise SetupError( - "You can't use both the `stream` and `obj_watch` options simultaneously." + if ack_policy is not EMPTY: + msg = "You can't use deprecated `ack_first` and `ack_policy` simultaneously. Please, use `ack_policy` only." + raise SetupError(msg) + + ack_policy = AckPolicy.ACK_FIRST if ack_first else AckPolicy.REJECT_ON_ERROR + + if no_ack is not EMPTY: + warnings.warn( + "`no_ack` option was deprecated in prior to `ack_policy=AckPolicy.DO_NOTHING`. Scheduled to remove in 0.7.0", + category=DeprecationWarning, + stacklevel=4, ) + if ack_policy is not EMPTY: + msg = "You can't use deprecated `no_ack` and `ack_policy` simultaneously. Please, use `ack_policy` only." + raise SetupError(msg) + + no_ack = AckPolicy.DO_NOTHING if no_ack else EMPTY + + if ack_policy is EMPTY: + ack_policy = AckPolicy.REJECT_ON_ERROR + + if stream and kv_watch: + msg = "You can't use both the `stream` and `kv_watch` options simultaneously." + raise SetupError(msg) + + if stream and obj_watch: + msg = "You can't use both the `stream` and `obj_watch` options simultaneously." + raise SetupError(msg) + if kv_watch and obj_watch: - raise SetupError( + msg = ( "You can't use both the `kv_watch` and `obj_watch` options simultaneously." ) + raise SetupError(msg) if pull_sub and not stream: - raise SetupError( - "The pull subscriber can only be used with the `stream` option." - ) + msg = "JetStream Pull Subscriber can only be used with the `stream` option." + raise SetupError(msg) - if max_msgs > 0 and any((stream, kv_watch, obj_watch)): - warnings.warn( - "The `max_msgs` option can be used only with a NATS Core Subscriber.", - RuntimeWarning, - stacklevel=4, - ) + if not subject and not config: + msg = "You must provide either the `subject` or `config` option." + raise SetupError(msg) if not stream: if obj_watch or kv_watch: @@ -449,49 +435,47 @@ def _validate_input_for_misconfigure( stacklevel=4, ) - if ack_first: + if ack_policy is AckPolicy.ACK_FIRST: warnings.warn( - message="The `ack_first` option can be used only with JetStream Push Subscription.", + message="The `ack_policy=AckPolicy.ACK_FIRST:` option can be used only with JetStream Push Subscription.", category=RuntimeWarning, stacklevel=4, ) - else: - # JetStream Subscribers - if pull_sub: - if queue: - warnings.warn( - message="The `queue` option has no effect with JetStream Pull Subscription. You probably wanted to use the `durable` option instead.", - category=RuntimeWarning, - stacklevel=4, - ) + # JetStream Subscribers + elif pull_sub: + if queue: + warnings.warn( + message="The `queue` option has no effect with JetStream Pull Subscription. You probably wanted to use the `durable` option instead.", + category=RuntimeWarning, + stacklevel=4, + ) - if ordered_consumer: - warnings.warn( - "The `ordered_consumer` option has no effect with JetStream Pull Subscription. It can only be used with JetStream Push Subscription.", - RuntimeWarning, - stacklevel=4, - ) + if ordered_consumer: + warnings.warn( + "The `ordered_consumer` option has no effect with JetStream Pull Subscription. It can only be used with JetStream Push Subscription.", + RuntimeWarning, + stacklevel=4, + ) - if ack_first: - warnings.warn( - message="The `ack_first` option has no effect with JetStream Pull Subscription. It can only be used with JetStream Push Subscription.", - category=RuntimeWarning, - stacklevel=4, - ) + if ack_policy is AckPolicy.ACK_FIRST: + warnings.warn( + message="The `ack_policy=AckPolicy.ACK_FIRST` option has no effect with JetStream Pull Subscription. It can only be used with JetStream Push Subscription.", + category=RuntimeWarning, + stacklevel=4, + ) - if flow_control: - warnings.warn( - message="The `flow_control` option has no effect with JetStream Pull Subscription. It can only be used with JetStream Push Subscription.", - category=RuntimeWarning, - stacklevel=4, - ) + if flow_control: + warnings.warn( + message="The `flow_control` option has no effect with JetStream Pull Subscription. It can only be used with JetStream Push Subscription.", + category=RuntimeWarning, + stacklevel=4, + ) - else: - # JS PushSub - if durable is not None: - warnings.warn( - message="The JetStream Push consumer with the `durable` option can't be scaled horizontally across multiple instances. You probably wanted to use the `queue` option instead. Also, we strongly recommend using the Jetstream PullSubsriber with the `durable` option as the default.", - category=RuntimeWarning, - stacklevel=4, - ) + # JS PushSub + elif durable is not None: + warnings.warn( + message="The JetStream Push consumer with the `durable` option can't be scaled horizontally across multiple instances. You probably wanted to use the `queue` option instead. Also, we strongly recommend using the Jetstream PullSubsriber with the `durable` option as the default.", + category=RuntimeWarning, + stacklevel=4, + ) diff --git a/faststream/nats/subscriber/specification.py b/faststream/nats/subscriber/specification.py new file mode 100644 index 0000000000..024d66fa1a --- /dev/null +++ b/faststream/nats/subscriber/specification.py @@ -0,0 +1,55 @@ +from faststream._internal.endpoint.subscriber import SubscriberSpecification +from faststream.nats.configs import NatsBrokerConfig +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 .config import NatsSubscriberSpecificationConfig + + +class NatsSubscriberSpecification(SubscriberSpecification[NatsBrokerConfig, NatsSubscriberSpecificationConfig]): + @property + def subject(self) -> str: + return f"{self._outer_config.prefix}{self.config.subject}" + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{self.subject}:{self.call_name}" + + def get_schema(self) -> dict[str, SubscriberSpec]: + payloads = self.get_payloads() + + return { + self.name: SubscriberSpec( + description=self.description, + operation=Operation( + message=Message( + title=f"{self.name}:Message", + payload=resolve_payloads(payloads), + ), + bindings=None, + ), + bindings=ChannelBinding( + nats=nats.ChannelBinding( + subject=self.subject, + queue=self.config.queue, + ), + ), + ), + } + + +class NotIncludeSpecifation(SubscriberSpecification): + @property + def include_in_schema(self) -> bool: + return False + + @property + def name(self) -> str: + raise NotImplementedError + + def get_schema(self) -> dict[str, "SubscriberSpec"]: + raise NotImplementedError diff --git a/faststream/nats/subscriber/state.py b/faststream/nats/subscriber/state.py new file mode 100644 index 0000000000..d8e2825d83 --- /dev/null +++ b/faststream/nats/subscriber/state.py @@ -0,0 +1,60 @@ +from typing import TYPE_CHECKING, Protocol + +from faststream.exceptions import IncorrectState + +if TYPE_CHECKING: + from nats.aio.client import Client + from nats.js import JetStreamContext + + from faststream.nats.broker.state import BrokerState + from faststream.nats.helpers import KVBucketDeclarer, OSBucketDeclarer + + +class SubscriberState(Protocol): + client: "Client" + js: "JetStreamContext" + kv_declarer: "KVBucketDeclarer" + os_declarer: "OSBucketDeclarer" + + +class EmptySubscriberState(SubscriberState): + @property + def client(self) -> "Client": + msg = "Connection is not available yet. Please, setup the subscriber first." + raise IncorrectState(msg) + + @property + def js(self) -> "JetStreamContext": + msg = "Stream is not available yet. Please, setup the subscriber first." + raise IncorrectState(msg) + + @property + def kv_declarer(self) -> "KVBucketDeclarer": + msg = "KeyValue is not available yet. Please, setup the subscriber first." + raise IncorrectState(msg) + + @property + def os_declarer(self) -> "OSBucketDeclarer": + msg = "ObjectStorage is not available yet. Please, setup the subscriber first." + raise IncorrectState(msg) + + +class ConnectedSubscriberState(SubscriberState): + def __init__( + self, + *, + parent_state: "BrokerState", + kv_declarer: "KVBucketDeclarer", + os_declarer: "OSBucketDeclarer", + ) -> None: + self._parent_state = parent_state + self.kv_declarer = kv_declarer + self.os_declarer = os_declarer + + @property + def client(self) -> "Client": + return self._parent_state.connection + + @property + def js(self) -> "JetStreamContext": + return self._parent_state.stream diff --git a/faststream/nats/subscriber/subscription.py b/faststream/nats/subscriber/subscription.py deleted file mode 100644 index 4bc994842b..0000000000 --- a/faststream/nats/subscriber/subscription.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Any, Generic, Optional, Protocol, TypeVar - - -class Unsubscriptable(Protocol): - async def unsubscribe(self) -> None: ... - - -class Watchable(Protocol): - async def stop(self) -> None: ... - - async def updates(self, timeout: float) -> Optional[Any]: ... - - -WatchableT = TypeVar("WatchableT", bound=Watchable) - - -class UnsubscribeAdapter(Unsubscriptable, Generic[WatchableT]): - __slots__ = ("obj",) - - obj: WatchableT - - def __init__(self, subscription: WatchableT) -> None: - self.obj = subscription - - async def unsubscribe(self) -> None: - await self.obj.stop() diff --git a/faststream/nats/subscriber/usecase.py b/faststream/nats/subscriber/usecase.py deleted file mode 100644 index defb922fed..0000000000 --- a/faststream/nats/subscriber/usecase.py +++ /dev/null @@ -1,1268 +0,0 @@ -from abc import abstractmethod -from contextlib import suppress -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Dict, - Generic, - Iterable, - List, - Optional, - Sequence, - TypeVar, - Union, - cast, -) - -import anyio -from fast_depends.dependencies import Depends -from nats.aio.msg import Msg -from nats.errors import ConnectionClosedError, TimeoutError -from nats.js.api import ConsumerConfig, ObjectInfo -from typing_extensions import Annotated, Doc, override - -from faststream.broker.publisher.fake import FakePublisher -from faststream.broker.subscriber.mixins import ConcurrentMixin, TasksMixin -from faststream.broker.subscriber.usecase import SubscriberUsecase -from faststream.broker.types import MsgType -from faststream.broker.utils import process_msg -from faststream.exceptions import NOT_CONNECTED_YET -from faststream.nats.parser import ( - BatchParser, - JsParser, - KvParser, - NatsParser, - ObjParser, -) -from faststream.nats.schemas.js_stream import compile_nats_wildcard -from faststream.nats.subscriber.subscription import ( - UnsubscribeAdapter, - Unsubscriptable, -) -from faststream.utils.context.repository import context - -if TYPE_CHECKING: - from nats.aio.client import Client - from nats.aio.subscription import Subscription - from nats.js import JetStreamContext - from nats.js.kv import KeyValue - from nats.js.object_store import ObjectStore - - from faststream.broker.message import StreamMessage - from faststream.broker.publisher.proto import ProducerProto - from faststream.broker.types import ( - AsyncCallable, - BrokerMiddleware, - CustomCallable, - ) - from faststream.nats.helpers import KVBucketDeclarer, OSBucketDeclarer - from faststream.nats.message import NatsKvMessage, NatsMessage, NatsObjMessage - from faststream.nats.schemas import JStream, KvWatch, ObjWatch, PullSub - from faststream.types import AnyDict, Decorator, LoggerProto, SendableMessage - - -ConnectionType = TypeVar("ConnectionType") - - -class LogicSubscriber(Generic[ConnectionType, MsgType], SubscriberUsecase[MsgType]): - """A class to represent a NATS handler.""" - - subscription: Optional[Unsubscriptable] - _fetch_sub: Optional[Unsubscriptable] - producer: Optional["ProducerProto"] - _connection: Optional[ConnectionType] - - def __init__( - self, - *, - subject: str, - config: "ConsumerConfig", - extra_options: Optional["AnyDict"], - # Subscriber args - default_parser: "AsyncCallable", - default_decoder: "AsyncCallable", - no_ack: bool, - no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable[Depends], - broker_middlewares: Sequence["BrokerMiddleware[MsgType]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - self.subject = subject - self.config = config - - self.extra_options = extra_options or {} - - super().__init__( - default_parser=default_parser, - default_decoder=default_decoder, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - self._connection = None - self._fetch_sub = None - self.subscription = None - self.producer = None - - @override - def setup( # type: ignore[override] - self, - *, - connection: ConnectionType, - # basic args - logger: Optional["LoggerProto"], - producer: Optional["ProducerProto"], - graceful_timeout: Optional[float], - extra_context: "AnyDict", - # broker options - broker_parser: Optional["CustomCallable"], - broker_decoder: Optional["CustomCallable"], - # dependant args - apply_types: bool, - is_validate: bool, - _get_dependant: Optional[Callable[..., Any]], - _call_decorators: Iterable["Decorator"], - ) -> None: - self._connection = connection - - super().setup( - logger=logger, - producer=producer, - graceful_timeout=graceful_timeout, - extra_context=extra_context, - broker_parser=broker_parser, - broker_decoder=broker_decoder, - apply_types=apply_types, - is_validate=is_validate, - _get_dependant=_get_dependant, - _call_decorators=_call_decorators, - ) - - @property - def clear_subject(self) -> str: - """Compile `test.{name}` to `test.*` subject.""" - _, path = compile_nats_wildcard(self.subject) - return path - - async def start(self) -> None: - """Create NATS subscription and start consume tasks.""" - assert self._connection, NOT_CONNECTED_YET # nosec B101 - - await super().start() - - if self.calls: - await self._create_subscription(connection=self._connection) - - async def close(self) -> None: - """Clean up handler subscription, cancel consume task in graceful mode.""" - await super().close() - - if self.subscription is not None: - await self.subscription.unsubscribe() - self.subscription = None - - if self._fetch_sub is not None: - await self._fetch_sub.unsubscribe() - self.subscription = None - - @abstractmethod - async def _create_subscription( - self, - *, - connection: ConnectionType, - ) -> None: - """Create NATS subscription object to consume messages.""" - raise NotImplementedError() - - @staticmethod - def build_log_context( - message: Annotated[ - Optional["StreamMessage[MsgType]"], - Doc("Message which we are building context for"), - ], - subject: Annotated[ - str, - Doc("NATS subject we are listening"), - ], - *, - queue: Annotated[ - str, - Doc("Using queue group name"), - ] = "", - stream: Annotated[ - str, - Doc("Stream object we are listening"), - ] = "", - ) -> Dict[str, str]: - """Static method to build log context out of `self.consume` scope.""" - return { - "subject": subject, - "queue": queue, - "stream": stream, - "message_id": getattr(message, "message_id", ""), - } - - def add_prefix(self, prefix: str) -> None: - """Include Subscriber in router.""" - if self.subject: - self.subject = "".join((prefix, self.subject)) - else: - self.config.filter_subjects = [ - "".join((prefix, subject)) - for subject in (self.config.filter_subjects or ()) - ] - - @property - def _resolved_subject_string(self) -> str: - return self.subject or ", ".join(self.config.filter_subjects or ()) - - def __hash__(self) -> int: - return self.get_routing_hash(self._resolved_subject_string) - - @staticmethod - def get_routing_hash( - subject: Annotated[ - str, - Doc("NATS subject to consume messages"), - ], - ) -> int: - """Get handler hash by outer data. - - Using to find handler in `broker.handlers` dictionary. - """ - return hash(subject) - - -class _DefaultSubscriber(LogicSubscriber[ConnectionType, MsgType]): - def __init__( - self, - *, - subject: str, - config: "ConsumerConfig", - # default args - extra_options: Optional["AnyDict"], - # Subscriber args - default_parser: "AsyncCallable", - default_decoder: "AsyncCallable", - no_ack: bool, - no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable[Depends], - broker_middlewares: Sequence["BrokerMiddleware[MsgType]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - super().__init__( - subject=subject, - config=config, - extra_options=extra_options, - # subscriber args - default_parser=default_parser, - default_decoder=default_decoder, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, - ) - - def _make_response_publisher( - self, - message: "StreamMessage[Any]", - ) -> Sequence[FakePublisher]: - """Create FakePublisher object to use it as one of `publishers` in `self.consume` scope.""" - if self._producer is None: - return () - - return ( - FakePublisher( - self._producer.publish, - publish_kwargs={ - "subject": message.reply_to, - }, - ), - ) - - def get_log_context( - self, - message: Annotated[ - Optional["StreamMessage[MsgType]"], - Doc("Message which we are building context for"), - ], - ) -> Dict[str, str]: - """Log context factory using in `self.consume` scope.""" - return self.build_log_context( - message=message, - subject=self.subject, - ) - - -class CoreSubscriber(_DefaultSubscriber["Client", "Msg"]): - subscription: Optional["Subscription"] - _fetch_sub: Optional["Subscription"] - - def __init__( - self, - *, - # default args - subject: str, - config: "ConsumerConfig", - queue: str, - extra_options: Optional["AnyDict"], - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable[Depends], - broker_middlewares: Sequence["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - parser_ = NatsParser(pattern=subject, no_ack=no_ack) - - self.queue = queue - - super().__init__( - subject=subject, - config=config, - extra_options=extra_options, - # subscriber args - default_parser=parser_.parse_message, - default_decoder=parser_.decode_message, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, - ) - - @override - async def get_one( - self, - *, - timeout: float = 5.0, - ) -> "Optional[NatsMessage]": - assert self._connection, "Please, start() subscriber first" # nosec B101 - assert ( # nosec B101 - not self.calls - ), "You can't use `get_one` method if subscriber has registered handlers." - - if self._fetch_sub is None: - fetch_sub = self._fetch_sub = await self._connection.subscribe( - subject=self.clear_subject, - queue=self.queue, - **self.extra_options, - ) - else: - fetch_sub = self._fetch_sub - - try: - raw_message = await fetch_sub.next_msg(timeout=timeout) - except TimeoutError: - return None - - msg: NatsMessage = await process_msg( # type: ignore[assignment] - msg=raw_message, - middlewares=self._broker_middlewares, - parser=self._parser, - decoder=self._decoder, - ) - return msg - - @override - async def _create_subscription( - self, - *, - connection: "Client", - ) -> None: - """Create NATS subscription and start consume task.""" - if self.subscription: - return - - self.subscription = await connection.subscribe( - subject=self.clear_subject, - queue=self.queue, - cb=self.consume, - **self.extra_options, - ) - - def get_log_context( - self, - message: Annotated[ - Optional["StreamMessage[Msg]"], - Doc("Message which we are building context for"), - ], - ) -> Dict[str, str]: - """Log context factory using in `self.consume` scope.""" - return self.build_log_context( - message=message, - subject=self.subject, - queue=self.queue, - ) - - -class ConcurrentCoreSubscriber( - ConcurrentMixin[Msg], - CoreSubscriber, -): - def __init__( - self, - *, - max_workers: int, - # default args - subject: str, - config: "ConsumerConfig", - queue: str, - extra_options: Optional["AnyDict"], - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable[Depends], - broker_middlewares: Sequence["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - super().__init__( - max_workers=max_workers, - # basic args - subject=subject, - config=config, - queue=queue, - extra_options=extra_options, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, - ) - - @override - async def _create_subscription( - self, - *, - connection: "Client", - ) -> None: - """Create NATS subscription and start consume task.""" - if self.subscription: - return - - self.start_consume_task() - - self.subscription = await connection.subscribe( - subject=self.clear_subject, - queue=self.queue, - cb=self._put_msg, - **self.extra_options, - ) - - -class _StreamSubscriber(_DefaultSubscriber["JetStreamContext", "Msg"]): - _fetch_sub: Optional["JetStreamContext.PullSubscription"] - - def __init__( - self, - *, - stream: "JStream", - # default args - subject: str, - config: "ConsumerConfig", - queue: str, - extra_options: Optional["AnyDict"], - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable[Depends], - broker_middlewares: Sequence["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - parser_ = JsParser(pattern=subject) - - self.queue = queue - self.stream = stream - - super().__init__( - subject=subject, - config=config, - extra_options=extra_options, - # subscriber args - default_parser=parser_.parse_message, - default_decoder=parser_.decode_message, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, - ) - - def get_log_context( - self, - message: Annotated[ - Optional["StreamMessage[Msg]"], - Doc("Message which we are building context for"), - ], - ) -> Dict[str, str]: - """Log context factory using in `self.consume` scope.""" - return self.build_log_context( - message=message, - subject=self._resolved_subject_string, - queue=self.queue, - stream=self.stream.name, - ) - - @override - async def get_one( - self, - *, - timeout: float = 5, - ) -> Optional["NatsMessage"]: - assert self._connection, "Please, start() subscriber first" # nosec B101 - assert ( # nosec B101 - not self.calls - ), "You can't use `get_one` method if subscriber has registered handlers." - - if not self._fetch_sub: - extra_options = { - "pending_bytes_limit": self.extra_options["pending_bytes_limit"], - "pending_msgs_limit": self.extra_options["pending_msgs_limit"], - "durable": self.extra_options["durable"], - "stream": self.extra_options["stream"], - } - if inbox_prefix := self.extra_options.get("inbox_prefix"): - extra_options["inbox_prefix"] = inbox_prefix - - self._fetch_sub = await self._connection.pull_subscribe( - subject=self.clear_subject, - config=self.config, - **extra_options, - ) - - try: - raw_message = ( - await self._fetch_sub.fetch( - batch=1, - timeout=timeout, - ) - )[0] - except (TimeoutError, ConnectionClosedError): - return None - - msg: NatsMessage = await process_msg( # type: ignore[assignment] - msg=raw_message, - middlewares=self._broker_middlewares, - parser=self._parser, - decoder=self._decoder, - ) - return msg - - -class PushStreamSubscription(_StreamSubscriber): - subscription: Optional["JetStreamContext.PushSubscription"] - - @override - async def _create_subscription( - self, - *, - connection: "JetStreamContext", - ) -> None: - """Create NATS subscription and start consume task.""" - if self.subscription: - return - - self.subscription = await connection.subscribe( - subject=self.clear_subject, - queue=self.queue, - cb=self.consume, - config=self.config, - **self.extra_options, - ) - - -class ConcurrentPushStreamSubscriber( - ConcurrentMixin[Msg], - _StreamSubscriber, -): - subscription: Optional["JetStreamContext.PushSubscription"] - - def __init__( - self, - *, - max_workers: int, - stream: "JStream", - # default args - subject: str, - config: "ConsumerConfig", - queue: str, - extra_options: Optional["AnyDict"], - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable[Depends], - broker_middlewares: Sequence["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - super().__init__( - max_workers=max_workers, - # basic args - stream=stream, - subject=subject, - config=config, - queue=queue, - extra_options=extra_options, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, - ) - - @override - async def _create_subscription( - self, - *, - connection: "JetStreamContext", - ) -> None: - """Create NATS subscription and start consume task.""" - if self.subscription: - return - - self.start_consume_task() - - self.subscription = await connection.subscribe( - subject=self.clear_subject, - queue=self.queue, - cb=self._put_msg, - config=self.config, - **self.extra_options, - ) - - -class PullStreamSubscriber(TasksMixin, _StreamSubscriber): - subscription: Optional["JetStreamContext.PullSubscription"] - - def __init__( - self, - *, - pull_sub: "PullSub", - stream: "JStream", - # default args - subject: str, - config: "ConsumerConfig", - extra_options: Optional["AnyDict"], - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable[Depends], - broker_middlewares: Sequence["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - self.pull_sub = pull_sub - - super().__init__( - # basic args - stream=stream, - subject=subject, - config=config, - extra_options=extra_options, - queue="", - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, - ) - - @override - async def _create_subscription( - self, - *, - connection: "JetStreamContext", - ) -> None: - """Create NATS subscription and start consume task.""" - if self.subscription: - return - - self.subscription = await connection.pull_subscribe( - subject=self.clear_subject, - config=self.config, - **self.extra_options, - ) - self.add_task(self._consume_pull(cb=self.consume)) - - async def _consume_pull( - self, - cb: Callable[["Msg"], Awaitable["SendableMessage"]], - ) -> None: - """Endless task consuming messages using NATS Pull subscriber.""" - assert self.subscription # nosec B101 - - while self.running: # pragma: no branch - messages = [] - with suppress(TimeoutError, ConnectionClosedError): - messages = await self.subscription.fetch( - batch=self.pull_sub.batch_size, - timeout=self.pull_sub.timeout, - ) - - if messages: - async with anyio.create_task_group() as tg: - for msg in messages: - tg.start_soon(cb, msg) - - -class ConcurrentPullStreamSubscriber( - ConcurrentMixin[Msg], - PullStreamSubscriber, -): - def __init__( - self, - *, - max_workers: int, - # default args - pull_sub: "PullSub", - stream: "JStream", - subject: str, - config: "ConsumerConfig", - extra_options: Optional["AnyDict"], - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable[Depends], - broker_middlewares: Sequence["BrokerMiddleware[Msg]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - super().__init__( - max_workers=max_workers, - # basic args - pull_sub=pull_sub, - stream=stream, - subject=subject, - config=config, - extra_options=extra_options, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, - ) - - @override - async def _create_subscription( - self, - *, - connection: "JetStreamContext", - ) -> None: - """Create NATS subscription and start consume task.""" - if self.subscription: - return - - self.start_consume_task() - - self.subscription = await connection.pull_subscribe( - subject=self.clear_subject, - config=self.config, - **self.extra_options, - ) - self.add_task(self._consume_pull(cb=self._put_msg)) - - -class BatchPullStreamSubscriber( - TasksMixin, - _DefaultSubscriber["JetStreamContext", List["Msg"]], -): - """Batch-message consumer class.""" - - subscription: Optional["JetStreamContext.PullSubscription"] - _fetch_sub: Optional["JetStreamContext.PullSubscription"] - - def __init__( - self, - *, - # default args - subject: str, - config: "ConsumerConfig", - stream: "JStream", - pull_sub: "PullSub", - extra_options: Optional["AnyDict"], - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable[Depends], - broker_middlewares: Sequence["BrokerMiddleware[List[Msg]]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - parser = BatchParser(pattern=subject) - - self.stream = stream - self.pull_sub = pull_sub - - super().__init__( - subject=subject, - config=config, - extra_options=extra_options, - # subscriber args - default_parser=parser.parse_batch, - default_decoder=parser.decode_batch, - # Propagated args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI args - description_=description_, - title_=title_, - include_in_schema=include_in_schema, - ) - - @override - async def get_one( - self, - *, - timeout: float = 5, - ) -> Optional["NatsMessage"]: - assert self._connection, "Please, start() subscriber first" # nosec B101 - assert ( # nosec B101 - not self.calls - ), "You can't use `get_one` method if subscriber has registered handlers." - - if not self._fetch_sub: - fetch_sub = self._fetch_sub = await self._connection.pull_subscribe( - subject=self.clear_subject, - config=self.config, - **self.extra_options, - ) - else: - fetch_sub = self._fetch_sub - - try: - raw_message = await fetch_sub.fetch( - batch=1, - timeout=timeout, - ) - except TimeoutError: - return None - - msg = cast( - "NatsMessage", - await process_msg( - msg=raw_message, - middlewares=self._broker_middlewares, - parser=self._parser, - decoder=self._decoder, - ), - ) - return msg - - @override - async def _create_subscription( - self, - *, - connection: "JetStreamContext", - ) -> None: - """Create NATS subscription and start consume task.""" - if self.subscription: - return - - self.subscription = await connection.pull_subscribe( - subject=self.clear_subject, - config=self.config, - **self.extra_options, - ) - self.add_task(self._consume_pull()) - - async def _consume_pull(self) -> None: - """Endless task consuming messages using NATS Pull subscriber.""" - assert self.subscription, "You should call `create_subscription` at first." # nosec B101 - - while self.running: # pragma: no branch - with suppress(TimeoutError, ConnectionClosedError): - messages = await self.subscription.fetch( - batch=self.pull_sub.batch_size, - timeout=self.pull_sub.timeout, - ) - - if messages: - await self.consume(messages) - - -class KeyValueWatchSubscriber( - TasksMixin, - LogicSubscriber["KVBucketDeclarer", "KeyValue.Entry"], -): - subscription: Optional["UnsubscribeAdapter[KeyValue.KeyWatcher]"] - _fetch_sub: Optional[UnsubscribeAdapter["KeyValue.KeyWatcher"]] - - def __init__( - self, - *, - subject: str, - config: "ConsumerConfig", - kv_watch: "KvWatch", - broker_dependencies: Iterable[Depends], - broker_middlewares: Sequence["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 - - super().__init__( - subject=subject, - config=config, - extra_options=None, - no_ack=True, - no_reply=True, - retry=False, - default_parser=parser.parse_message, - 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 - async def get_one( - self, - *, - timeout: float = 5, - ) -> Optional["NatsKvMessage"]: - assert self._connection, "Please, start() subscriber first" # nosec B101 - assert ( # nosec B101 - not self.calls - ), "You can't use `get_one` method if subscriber has registered handlers." - - if not self._fetch_sub: - bucket = await self._connection.create_key_value( - bucket=self.kv_watch.name, - declare=self.kv_watch.declare, - ) - - fetch_sub = self._fetch_sub = UnsubscribeAdapter["KeyValue.KeyWatcher"]( - await bucket.watch( - keys=self.clear_subject, - headers_only=self.kv_watch.headers_only, - include_history=self.kv_watch.include_history, - ignore_deletes=self.kv_watch.ignore_deletes, - meta_only=self.kv_watch.meta_only, - ) - ) - else: - fetch_sub = self._fetch_sub - - raw_message: Optional[KeyValue.Entry] = None - sleep_interval = timeout / 10 - with anyio.move_on_after(timeout): - while ( # noqa: ASYNC110 - raw_message := await fetch_sub.obj.updates(timeout) # type: ignore[no-untyped-call] - ) is None: - await anyio.sleep(sleep_interval) - - return await process_msg( # type: ignore[return-value] - msg=raw_message, - middlewares=self._broker_middlewares, - parser=self._parser, - decoder=self._decoder, - ) - - @override - async def _create_subscription( - self, - *, - connection: "KVBucketDeclarer", - ) -> None: - if self.subscription: - return - - bucket = await connection.create_key_value( - bucket=self.kv_watch.name, - declare=self.kv_watch.declare, - ) - - self.subscription = UnsubscribeAdapter["KeyValue.KeyWatcher"]( - await bucket.watch( - keys=self.clear_subject, - headers_only=self.kv_watch.headers_only, - include_history=self.kv_watch.include_history, - ignore_deletes=self.kv_watch.ignore_deletes, - meta_only=self.kv_watch.meta_only, - ) - ) - - self.add_task(self._consume_watch()) - - async def _consume_watch(self) -> None: - assert self.subscription, "You should call `create_subscription` at first." # nosec B101 - - key_watcher = self.subscription.obj - - while self.running: - with suppress(ConnectionClosedError, TimeoutError): - message = cast( - "Optional[KeyValue.Entry]", - await key_watcher.updates(self.kv_watch.timeout), # type: ignore[no-untyped-call] - ) - - if message: - await self.consume(message) - - def _make_response_publisher( - self, - message: Annotated[ - "StreamMessage[KeyValue.Entry]", - Doc("Message requiring reply"), - ], - ) -> Sequence[FakePublisher]: - """Create FakePublisher object to use it as one of `publishers` in `self.consume` scope.""" - return () - - def __hash__(self) -> int: - return hash(self.kv_watch) + hash(self.subject) - - def get_log_context( - self, - message: Annotated[ - Optional["StreamMessage[KeyValue.Entry]"], - Doc("Message which we are building context for"), - ], - ) -> Dict[str, str]: - """Log context factory using in `self.consume` scope.""" - return self.build_log_context( - message=message, - subject=self.subject, - stream=self.kv_watch.name, - ) - - -OBJECT_STORAGE_CONTEXT_KEY = "__object_storage" - - -class ObjStoreWatchSubscriber( - TasksMixin, - LogicSubscriber["OSBucketDeclarer", ObjectInfo], -): - subscription: Optional["UnsubscribeAdapter[ObjectStore.ObjectWatcher]"] - _fetch_sub: Optional[UnsubscribeAdapter["ObjectStore.ObjectWatcher"]] - - def __init__( - self, - *, - subject: str, - config: "ConsumerConfig", - obj_watch: "ObjWatch", - broker_dependencies: Iterable[Depends], - broker_middlewares: Sequence["BrokerMiddleware[List[Msg]]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - parser = ObjParser(pattern="") - - self.obj_watch = obj_watch - self.obj_watch_conn = None - - super().__init__( - subject=subject, - config=config, - extra_options=None, - no_ack=True, - no_reply=True, - retry=False, - default_parser=parser.parse_message, - 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 - async def get_one( - self, - *, - timeout: float = 5, - ) -> Optional["NatsObjMessage"]: - assert self._connection, "Please, start() subscriber first" # nosec B101 - assert ( # nosec B101 - not self.calls - ), "You can't use `get_one` method if subscriber has registered handlers." - - if not self._fetch_sub: - self.bucket = await self._connection.create_object_store( - bucket=self.subject, - declare=self.obj_watch.declare, - ) - - obj_watch = await self.bucket.watch( - ignore_deletes=self.obj_watch.ignore_deletes, - include_history=self.obj_watch.include_history, - meta_only=self.obj_watch.meta_only, - ) - fetch_sub = self._fetch_sub = UnsubscribeAdapter[ - "ObjectStore.ObjectWatcher" - ](obj_watch) - else: - fetch_sub = self._fetch_sub - - raw_message: Optional[ObjectInfo] = None - sleep_interval = timeout / 10 - with anyio.move_on_after(timeout): - while ( # noqa: ASYNC110 - raw_message := await fetch_sub.obj.updates(timeout) # type: ignore[no-untyped-call] - ) is None: - await anyio.sleep(sleep_interval) - - return await process_msg( # type: ignore[return-value] - msg=raw_message, - middlewares=self._broker_middlewares, - parser=self._parser, - decoder=self._decoder, - ) - - @override - async def _create_subscription( - self, - *, - connection: "OSBucketDeclarer", - ) -> None: - if self.subscription: - return - - self.bucket = await connection.create_object_store( - bucket=self.subject, - declare=self.obj_watch.declare, - ) - - self.add_task(self._consume_watch()) - - async def _consume_watch(self) -> None: - assert self.bucket, "You should call `create_subscription` at first." # nosec B101 - - # Should be created inside task to avoid nats-py lock - obj_watch = await self.bucket.watch( - ignore_deletes=self.obj_watch.ignore_deletes, - include_history=self.obj_watch.include_history, - meta_only=self.obj_watch.meta_only, - ) - - self.subscription = UnsubscribeAdapter["ObjectStore.ObjectWatcher"](obj_watch) - - while self.running: - with suppress(TimeoutError): - message = cast( - "Optional[ObjectInfo]", - await obj_watch.updates(self.obj_watch.timeout), # type: ignore[no-untyped-call] - ) - - if message: - with context.scope(OBJECT_STORAGE_CONTEXT_KEY, self.bucket): - await self.consume(message) - - def _make_response_publisher( - self, - message: Annotated[ - "StreamMessage[ObjectInfo]", - Doc("Message requiring reply"), - ], - ) -> Sequence[FakePublisher]: - """Create FakePublisher object to use it as one of `publishers` in `self.consume` scope.""" - return () - - def __hash__(self) -> int: - return hash(self.subject) - - def get_log_context( - self, - message: Annotated[ - Optional["StreamMessage[ObjectInfo]"], - Doc("Message which we are building context for"), - ], - ) -> Dict[str, str]: - """Log context factory using in `self.consume` scope.""" - return self.build_log_context( - message=message, - subject=self.subject, - ) diff --git a/faststream/nats/subscriber/usecases/__init__.py b/faststream/nats/subscriber/usecases/__init__.py new file mode 100644 index 0000000000..5bf2613e7c --- /dev/null +++ b/faststream/nats/subscriber/usecases/__init__.py @@ -0,0 +1,26 @@ +from .basic import LogicSubscriber +from .core_subscriber import ConcurrentCoreSubscriber, CoreSubscriber +from .key_value_subscriber import KeyValueWatchSubscriber +from .object_storage_subscriber import ObjStoreWatchSubscriber +from .stream_pull_subscriber import ( + BatchPullStreamSubscriber, + ConcurrentPullStreamSubscriber, + PullStreamSubscriber, +) +from .stream_push_subscriber import ( + ConcurrentPushStreamSubscriber, + PushStreamSubscriber, +) + +__all__ = ( + "BatchPullStreamSubscriber", + "ConcurrentCoreSubscriber", + "ConcurrentPullStreamSubscriber", + "ConcurrentPushStreamSubscriber", + "CoreSubscriber", + "KeyValueWatchSubscriber", + "LogicSubscriber", + "ObjStoreWatchSubscriber", + "PullStreamSubscriber", + "PushStreamSubscriber", +) diff --git a/faststream/nats/subscriber/usecases/basic.py b/faststream/nats/subscriber/usecases/basic.py new file mode 100644 index 0000000000..2f450c4b83 --- /dev/null +++ b/faststream/nats/subscriber/usecases/basic.py @@ -0,0 +1,145 @@ +from abc import abstractmethod +from collections.abc import Iterable +from typing import ( + TYPE_CHECKING, + Any, + Optional, +) + +from faststream._internal.endpoint.subscriber.usecase import SubscriberUsecase +from faststream._internal.types import MsgType +from faststream.nats.publisher.fake import NatsFakePublisher +from faststream.nats.schemas.js_stream import compile_nats_wildcard +from faststream.nats.subscriber.adapters import ( + Unsubscriptable, +) + +if TYPE_CHECKING: + from nats.aio.client import Client + from nats.js import JetStreamContext + + from faststream._internal.endpoint.publisher import BasePublisherProto + from faststream._internal.endpoint.subscriber.call_item import CallsCollection + from faststream.message import StreamMessage + from faststream.nats.configs import NatsBrokerConfig + from faststream.nats.subscriber.config import NatsSubscriberConfig + from faststream.nats.subscriber.specification import NatsSubscriberSpecification + + +class LogicSubscriber(SubscriberUsecase[MsgType]): + """Basic class for all NATS Subscriber types (KeyValue, ObjectStorage, Core & JetStream).""" + + subscription: Unsubscriptable | None + _fetch_sub: Unsubscriptable | None + _outer_config: "NatsBrokerConfig" + + def __init__( + self, + config: "NatsSubscriberConfig", + specification: "NatsSubscriberSpecification", + calls: "CallsCollection", + ) -> None: + super().__init__(config, specification, calls) + + self._subject = config.subject + self.config = config.sub_config + + self.extra_options = config.extra_options or {} + + self._fetch_sub = None + self.subscription = None + + @property + def subject(self) -> str: + return f"{self._outer_config.prefix}{self._subject}" + + @property + def filter_subjects(self) -> list[str]: + prefix = self._outer_config.prefix + return [f"{prefix}{subject}" for subject in (self.config.filter_subjects or ())] + + @property + def clear_subject(self) -> str: + """Compile `test.{name}` to `test.*` subject.""" + _, path = compile_nats_wildcard(self.subject) + return path + + @property + def connection(self) -> "Client": + return self._outer_config.connection_state.connection + + @property + def jetstream(self) -> "JetStreamContext": + return self._outer_config.connection_state.stream + + async def start(self) -> None: + """Create NATS subscription and start consume tasks.""" + await super().start() + + if self.calls: + await self._create_subscription() + + self._post_start() + + async def close(self) -> None: + """Clean up handler subscription, cancel consume task in graceful mode.""" + await super().close() + + if self.subscription is not None: + await self.subscription.unsubscribe() + self.subscription = None + + if self._fetch_sub is not None: + await self._fetch_sub.unsubscribe() + self.subscription = None + + @abstractmethod + async def _create_subscription(self) -> None: + """Create NATS subscription object to consume messages.""" + raise NotImplementedError + + @staticmethod + def build_log_context( + message: Optional["StreamMessage[MsgType]"], + subject: str, + *, + queue: str = "", + stream: str = "", + ) -> dict[str, str]: + """Static method to build log context out of `self.consume` scope.""" + return { + "subject": subject, + "queue": queue, + "stream": stream, + "message_id": getattr(message, "message_id", ""), + } + + @property + def _resolved_subject_string(self) -> str: + return self.subject or ", ".join(self.filter_subjects or ()) + + +class DefaultSubscriber(LogicSubscriber[MsgType]): + """Basic class for Core & JetStream Subscribers.""" + + def _make_response_publisher( + self, + message: "StreamMessage[Any]", + ) -> Iterable["BasePublisherProto"]: + """Create Publisher objects to use it as one of `publishers` in `self.consume` scope.""" + return ( + NatsFakePublisher( + producer=self._outer_config.producer, + subject=message.reply_to, + ), + ) + + def get_log_context( + self, + message: Optional["StreamMessage[MsgType]"], + ) -> dict[str, str]: + """Log context factory using in `self.consume` scope.""" + return self.build_log_context( + message=message, + subject=self.subject, + ) diff --git a/faststream/nats/subscriber/usecases/core_subscriber.py b/faststream/nats/subscriber/usecases/core_subscriber.py new file mode 100644 index 0000000000..d3991bc490 --- /dev/null +++ b/faststream/nats/subscriber/usecases/core_subscriber.py @@ -0,0 +1,158 @@ +from collections.abc import AsyncIterator +from typing import ( + TYPE_CHECKING, + Annotated, + Optional, +) + +from nats.errors import TimeoutError +from typing_extensions import Doc, override + +from faststream._internal.endpoint.subscriber.mixins import ConcurrentMixin +from faststream._internal.endpoint.utils import process_msg +from faststream.middlewares import AckPolicy +from faststream.nats.parser import NatsParser + +from .basic import DefaultSubscriber + +if TYPE_CHECKING: + from nats.aio.msg import Msg + from nats.aio.subscription import Subscription + + from faststream._internal.endpoint.subscriber.call_item import CallsCollection + from faststream.message import StreamMessage + from faststream.nats.message import NatsMessage + from faststream.nats.subscriber.config import ( + NatsSubscriberConfig, + NatsSubscriberSpecificationConfig, + ) + + +class CoreSubscriber(DefaultSubscriber["Msg"]): + subscription: Optional["Subscription"] + _fetch_sub: Optional["Subscription"] + + def __init__( + self, + config: "NatsSubscriberConfig", + specification: "NatsSubscriberSpecificationConfig", + calls: "CallsCollection", + *, + queue: str, + ) -> None: + parser = NatsParser( + pattern=config.subject, + is_ack_disabled=config.ack_policy is not AckPolicy.DO_NOTHING, + ) + config.parser = parser.parse_message + config.decoder = parser.decode_message + super().__init__(config, specification, calls) + + self.queue = queue + + @override + async def get_one( + self, + *, + timeout: float = 5.0, + ) -> "NatsMessage | None": + assert ( # nosec B101 + not self.calls + ), "You can't use `get_one` method if subscriber has registered handlers." + + if self._fetch_sub is None: + fetch_sub = self._fetch_sub = await self.connection.subscribe( + subject=self.clear_subject, + queue=self.queue, + **self.extra_options, + ) + else: + fetch_sub = self._fetch_sub + + try: + raw_message = await fetch_sub.next_msg(timeout=timeout) + except TimeoutError: + return None + + context = self._outer_config.fd_config.context + + msg: NatsMessage = await process_msg( # type: ignore[assignment] + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + return msg + + @override + async def __aiter__(self) -> AsyncIterator["NatsMessage"]: # type: ignore[override] + assert ( # nosec B101 + not self.calls + ), "You can't use iterator if subscriber has registered handlers." + + if self._fetch_sub is None: + fetch_sub = self._fetch_sub = await self.connection.subscribe( + subject=self.clear_subject, + queue=self.queue, + **self.extra_options, + ) + else: + fetch_sub = self._fetch_sub + + async for raw_message in fetch_sub.messages: + context = self._outer_config.fd_config.context + + msg: NatsMessage = await process_msg( # type: ignore[assignment] + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + yield msg + + async def _create_subscription(self) -> None: + """Create NATS subscription and start consume task.""" + if self.subscription: + return + + self.subscription = await self.connection.subscribe( + subject=self.clear_subject, + queue=self.queue, + cb=self.consume, + **self.extra_options, + ) + + def get_log_context( + self, + message: Annotated[ + Optional["StreamMessage[Msg]"], + Doc("Message which we are building context for"), + ], + ) -> dict[str, str]: + """Log context factory using in `self.consume` scope.""" + return self.build_log_context( + message=message, + subject=self.subject, + queue=self.queue, + ) + + +class ConcurrentCoreSubscriber(ConcurrentMixin["Msg"], CoreSubscriber): + @override + async def _create_subscription(self) -> None: + """Create NATS subscription and start consume task.""" + if self.subscription: + return + + self.start_consume_task() + + self.subscription = await self.connection.subscribe( + subject=self.clear_subject, + queue=self.queue, + cb=self._put_msg, + **self.extra_options, + ) diff --git a/faststream/nats/subscriber/usecases/key_value_subscriber.py b/faststream/nats/subscriber/usecases/key_value_subscriber.py new file mode 100644 index 0000000000..317aca08b8 --- /dev/null +++ b/faststream/nats/subscriber/usecases/key_value_subscriber.py @@ -0,0 +1,217 @@ +from collections.abc import AsyncIterator, Iterable +from contextlib import suppress +from typing import ( + TYPE_CHECKING, + Annotated, + Optional, + cast, +) + +import anyio +from nats.errors import ConnectionClosedError, TimeoutError +from typing_extensions import Doc, override + +from faststream._internal.endpoint.subscriber.mixins import TasksMixin +from faststream._internal.endpoint.utils import process_msg +from faststream.nats.parser import KvParser +from faststream.nats.subscriber.adapters import UnsubscribeAdapter + +from .basic import LogicSubscriber + +if TYPE_CHECKING: + from nats.js.kv import KeyValue + + from faststream._internal.endpoint.publisher import BasePublisherProto + from faststream._internal.endpoint.subscriber.call_item import CallsCollection + from faststream.message import StreamMessage + from faststream.nats.message import NatsKvMessage + from faststream.nats.schemas import KvWatch + from faststream.nats.subscriber.config import ( + NatsSubscriberConfig, + NatsSubscriberSpecificationConfig, + ) + + +class KeyValueWatchSubscriber( + TasksMixin, + LogicSubscriber["KeyValue.Entry"], +): + subscription: Optional["UnsubscribeAdapter[KeyValue.KeyWatcher]"] + _fetch_sub: UnsubscribeAdapter["KeyValue.KeyWatcher"] | None + + def __init__( + self, + config: "NatsSubscriberConfig", + specification: "NatsSubscriberSpecificationConfig", + calls: "CallsCollection", + *, + kv_watch: "KvWatch", + ) -> None: + parser = KvParser(pattern=config.subject) + config.decoder = parser.decode_message + config.parser = parser.parse_message + super().__init__(config, specification, calls) + + self.kv_watch = kv_watch + + @override + async def get_one( + self, + *, + timeout: float = 5, + ) -> Optional["NatsKvMessage"]: + assert ( # nosec B101 + not self.calls + ), "You can't use `get_one` method if subscriber has registered handlers." + + if not self._fetch_sub: + bucket = await self._outer_config.kv_declarer.create_key_value( + bucket=self.kv_watch.name, + declare=self.kv_watch.declare, + ) + + fetch_sub = self._fetch_sub = UnsubscribeAdapter["KeyValue.KeyWatcher"]( + await bucket.watch( + keys=self.clear_subject, + headers_only=self.kv_watch.headers_only, + include_history=self.kv_watch.include_history, + ignore_deletes=self.kv_watch.ignore_deletes, + meta_only=self.kv_watch.meta_only, + ), + ) + else: + fetch_sub = self._fetch_sub + + raw_message = None + sleep_interval = timeout / 10 + with anyio.move_on_after(timeout): + while ( # noqa: ASYNC110 + # type: ignore[no-untyped-call] + raw_message := await fetch_sub.obj.updates(timeout) + ) is None: + await anyio.sleep(sleep_interval) + + context = self._outer_config.fd_config.context + + msg: NatsKvMessage = await process_msg( + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + return msg + + @override + async def __aiter__(self) -> AsyncIterator["NatsKvMessage"]: # type: ignore[override] + assert ( # nosec B101 + not self.calls + ), "You can't use iterator if subscriber has registered handlers." + + if not self._fetch_sub: + bucket = await self._outer_config.kv_declarer.create_key_value( + bucket=self.kv_watch.name, + declare=self.kv_watch.declare, + ) + + fetch_sub = self._fetch_sub = UnsubscribeAdapter["KeyValue.KeyWatcher"]( + await bucket.watch( + keys=self.clear_subject, + headers_only=self.kv_watch.headers_only, + include_history=self.kv_watch.include_history, + ignore_deletes=self.kv_watch.ignore_deletes, + meta_only=self.kv_watch.meta_only, + ), + ) + else: + fetch_sub = self._fetch_sub + + timeout = 5 + sleep_interval = timeout / 10 + + while True: + raw_message = None + with anyio.move_on_after(timeout): + while ( # noqa: ASYNC110 + # type: ignore[no-untyped-call] + raw_message := await fetch_sub.obj.updates(timeout) + ) is None: + await anyio.sleep(sleep_interval) + + if raw_message is None: + continue + + context = self._outer_config.fd_config.context + + msg: NatsKvMessage = await process_msg( # type: ignore[assignment] + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + yield msg + + @override + async def _create_subscription(self) -> None: + if self.subscription: + return + + bucket = await self._outer_config.kv_declarer.create_key_value( + bucket=self.kv_watch.name, + declare=self.kv_watch.declare, + ) + + self.subscription = UnsubscribeAdapter["KeyValue.KeyWatcher"]( + await bucket.watch( + keys=self.clear_subject, + headers_only=self.kv_watch.headers_only, + include_history=self.kv_watch.include_history, + ignore_deletes=self.kv_watch.ignore_deletes, + meta_only=self.kv_watch.meta_only, + ), + ) + + self.add_task(self.__consume_watch()) + + async def __consume_watch(self) -> None: + assert self.subscription, "You should call `create_subscription` at first." # nosec B101 + + key_watcher = self.subscription.obj + + while self.running: + with suppress(ConnectionClosedError, TimeoutError): + message = cast( + "KeyValue.Entry | None", + # type: ignore[no-untyped-call] + await key_watcher.updates(self.kv_watch.timeout), + ) + + if message: + await self.consume(message) + + def _make_response_publisher( + self, + message: Annotated[ + "StreamMessage[KeyValue.Entry]", + Doc("Message requiring reply"), + ], + ) -> Iterable["BasePublisherProto"]: + """Create Publisher objects to use it as one of `publishers` in `self.consume` scope.""" + return () + + def get_log_context( + self, + message: Annotated[ + Optional["StreamMessage[KeyValue.Entry]"], + Doc("Message which we are building context for"), + ], + ) -> dict[str, str]: + """Log context factory using in `self.consume` scope.""" + return self.build_log_context( + message=message, + subject=self.subject, + stream=self.kv_watch.name, + ) diff --git a/faststream/nats/subscriber/usecases/object_storage_subscriber.py b/faststream/nats/subscriber/usecases/object_storage_subscriber.py new file mode 100644 index 0000000000..fbe06663b8 --- /dev/null +++ b/faststream/nats/subscriber/usecases/object_storage_subscriber.py @@ -0,0 +1,220 @@ +from collections.abc import AsyncIterator, Iterable +from contextlib import suppress +from typing import ( + TYPE_CHECKING, + Annotated, + Optional, + cast, +) + +import anyio +from nats.errors import TimeoutError +from nats.js.api import ObjectInfo +from typing_extensions import Doc, override + +from faststream._internal.endpoint.subscriber.mixins import TasksMixin +from faststream._internal.endpoint.utils import process_msg +from faststream.nats.parser import ( + ObjParser, +) +from faststream.nats.subscriber.adapters import ( + UnsubscribeAdapter, +) + +from .basic import LogicSubscriber + +if TYPE_CHECKING: + from nats.js.object_store import ObjectStore + + from faststream._internal.endpoint.publisher import BasePublisherProto + from faststream._internal.endpoint.subscriber.call_item import CallsCollection + from faststream.message import StreamMessage + from faststream.nats.message import NatsObjMessage + from faststream.nats.schemas import ObjWatch + from faststream.nats.subscriber.config import ( + NatsSubscriberConfig, + NatsSubscriberSpecificationConfig, + ) + +OBJECT_STORAGE_CONTEXT_KEY = "__object_storage" + + +class ObjStoreWatchSubscriber( + TasksMixin, + LogicSubscriber[ObjectInfo], +): + subscription: Optional["UnsubscribeAdapter[ObjectStore.ObjectWatcher]"] + _fetch_sub: UnsubscribeAdapter["ObjectStore.ObjectWatcher"] | None + + def __init__( + self, + config: "NatsSubscriberConfig", + specification: "NatsSubscriberSpecificationConfig", + calls: "CallsCollection", + *, + obj_watch: "ObjWatch", + ) -> None: + parser = ObjParser(pattern="") + config.parser = parser.parse_message + config.decoder = parser.decode_message + super().__init__(config, specification, calls) + + self.obj_watch = obj_watch + self.obj_watch_conn = None + + @override + async def get_one( + self, + *, + timeout: float = 5, + ) -> Optional["NatsObjMessage"]: + assert ( # nosec B101 + not self.calls + ), "You can't use `get_one` method if subscriber has registered handlers." + + if not self._fetch_sub: + self.bucket = await self._outer_config.os_declarer.create_object_store( + bucket=self.subject, + declare=self.obj_watch.declare, + ) + + obj_watch = await self.bucket.watch( + ignore_deletes=self.obj_watch.ignore_deletes, + include_history=self.obj_watch.include_history, + meta_only=self.obj_watch.meta_only, + ) + fetch_sub = self._fetch_sub = UnsubscribeAdapter[ + "ObjectStore.ObjectWatcher" + ](obj_watch) + else: + fetch_sub = self._fetch_sub + + raw_message = None + sleep_interval = timeout / 10 + with anyio.move_on_after(timeout): + while ( # noqa: ASYNC110 + # type: ignore[no-untyped-call] + raw_message := await fetch_sub.obj.updates(timeout) + ) is None: + await anyio.sleep(sleep_interval) + + context = self._outer_config.fd_config.context + + msg: NatsObjMessage = await process_msg( + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + return msg + + @override + async def __aiter__(self) -> AsyncIterator["NatsObjMessage"]: # type: ignore[override] + assert ( # nosec B101 + not self.calls + ), "You can't use iterator if subscriber has registered handlers." + + if not self._fetch_sub: + self.bucket = await self._outer_config.os_declarer.create_object_store( + bucket=self.subject, + declare=self.obj_watch.declare, + ) + + obj_watch = await self.bucket.watch( + ignore_deletes=self.obj_watch.ignore_deletes, + include_history=self.obj_watch.include_history, + meta_only=self.obj_watch.meta_only, + ) + fetch_sub = self._fetch_sub = UnsubscribeAdapter[ + "ObjectStore.ObjectWatcher" + ](obj_watch) + else: + fetch_sub = self._fetch_sub + + timeout = 5 + sleep_interval = timeout / 10 + while True: + raw_message = None + with anyio.move_on_after(timeout): + while ( # noqa: ASYNC110 + # type: ignore[no-untyped-call] + raw_message := await fetch_sub.obj.updates(timeout) + ) is None: + await anyio.sleep(sleep_interval) + + if raw_message is None: + continue + + context = self._outer_config.fd_config.context + + msg: NatsObjMessage = await process_msg( # type: ignore[assignment] + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + yield msg + + @override + async def _create_subscription(self) -> None: + if self.subscription: + return + + self.bucket = await self._outer_config.os_declarer.create_object_store( + bucket=self.subject, + declare=self.obj_watch.declare, + ) + + self.add_task(self.__consume_watch()) + + async def __consume_watch(self) -> None: + assert self.bucket, "You should call `create_subscription` at first." # nosec B101 + + # Should be created inside task to avoid nats-py lock + obj_watch = await self.bucket.watch( + ignore_deletes=self.obj_watch.ignore_deletes, + include_history=self.obj_watch.include_history, + meta_only=self.obj_watch.meta_only, + ) + + self.subscription = UnsubscribeAdapter["ObjectStore.ObjectWatcher"](obj_watch) + + context = self._outer_config.fd_config.context + + while self.running: + with suppress(TimeoutError): + message = cast( + "ObjectInfo | None", + await obj_watch.updates(self.obj_watch.timeout), # type: ignore[no-untyped-call] + ) + + if message: + with context.scope(OBJECT_STORAGE_CONTEXT_KEY, self.bucket): + await self.consume(message) + + def _make_response_publisher( + self, + message: Annotated[ + "StreamMessage[ObjectInfo]", + Doc("Message requiring reply"), + ], + ) -> Iterable["BasePublisherProto"]: + """Create Publisher objects to use it as one of `publishers` in `self.consume` scope.""" + return () + + def get_log_context( + self, + message: Annotated[ + Optional["StreamMessage[ObjectInfo]"], + Doc("Message which we are building context for"), + ], + ) -> dict[str, str]: + """Log context factory using in `self.consume` scope.""" + return self.build_log_context( + message=message, + subject=self.subject, + ) diff --git a/faststream/nats/subscriber/usecases/stream_basic.py b/faststream/nats/subscriber/usecases/stream_basic.py new file mode 100644 index 0000000000..eeafcfc1a2 --- /dev/null +++ b/faststream/nats/subscriber/usecases/stream_basic.py @@ -0,0 +1,153 @@ +from collections.abc import AsyncIterator +from typing import ( + TYPE_CHECKING, + Annotated, + Optional, +) + +from nats.errors import ConnectionClosedError, TimeoutError +from typing_extensions import Doc, override + +from faststream._internal.endpoint.utils import process_msg +from faststream.nats.parser import JsParser + +from .basic import DefaultSubscriber + +if TYPE_CHECKING: + from nats.aio.msg import Msg + from nats.js import JetStreamContext + + from faststream._internal.endpoint.subscriber.call_item import CallsCollection + from faststream.message import StreamMessage + from faststream.nats.message import NatsMessage + from faststream.nats.schemas import JStream + from faststream.nats.subscriber.config import ( + NatsSubscriberConfig, + NatsSubscriberSpecificationConfig, + ) + + +class StreamSubscriber(DefaultSubscriber["Msg"]): + _fetch_sub: Optional["JetStreamContext.PullSubscription"] + + def __init__( + self, + config: "NatsSubscriberConfig", + specification: "NatsSubscriberSpecificationConfig", + calls: "CallsCollection", + *, + stream: "JStream", + queue: str, + ) -> None: + parser = JsParser(pattern=config.subject) + config.decoder = parser.decode_message + config.parser = parser.parse_message + super().__init__(config, specification, calls) + + self.queue = queue + self.stream = stream + + def get_log_context( + self, + message: Annotated[ + Optional["StreamMessage[Msg]"], + Doc("Message which we are building context for"), + ], + ) -> dict[str, str]: + """Log context factory using in `self.consume` scope.""" + return self.build_log_context( + message=message, + subject=self._resolved_subject_string, + queue=self.queue, + stream=self.stream.name, + ) + + @override + async def get_one( + self, + *, + timeout: float = 5, + ) -> Optional["NatsMessage"]: + assert ( # nosec B101 + not self.calls + ), "You can't use `get_one` method if subscriber has registered handlers." + + if not self._fetch_sub: + extra_options = { + "pending_bytes_limit": self.extra_options["pending_bytes_limit"], + "pending_msgs_limit": self.extra_options["pending_msgs_limit"], + "durable": self.extra_options["durable"], + "stream": self.extra_options["stream"], + } + if inbox_prefix := self.extra_options.get("inbox_prefix"): + extra_options["inbox_prefix"] = inbox_prefix + + self._fetch_sub = await self.jetstream.pull_subscribe( + subject=self.clear_subject, + config=self.config, + **extra_options, + ) + + try: + raw_message = ( + await self._fetch_sub.fetch( + batch=1, + timeout=timeout, + ) + )[0] + except (TimeoutError, ConnectionClosedError): + return None + + context = self._outer_config.fd_config.context + + msg: NatsMessage = await process_msg( # type: ignore[assignment] + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + return msg + + @override + async def __aiter__(self) -> AsyncIterator["NatsMessage"]: # type: ignore[override] + assert ( # nosec B101 + not self.calls + ), "You can't use iterator if subscriber has registered handlers." + + if not self._fetch_sub: + extra_options = { + "pending_bytes_limit": self.extra_options["pending_bytes_limit"], + "pending_msgs_limit": self.extra_options["pending_msgs_limit"], + "durable": self.extra_options["durable"], + "stream": self.extra_options["stream"], + } + if inbox_prefix := self.extra_options.get("inbox_prefix"): + extra_options["inbox_prefix"] = inbox_prefix + + self._fetch_sub = await self.jetstream.pull_subscribe( + subject=self.clear_subject, + config=self.config, + **extra_options, + ) + + while True: + raw_message = ( + await self._fetch_sub.fetch( + batch=1, + timeout=None, + ) + )[0] + + context = self._outer_config.fd_config.context + + msg: NatsMessage = await process_msg( # type: ignore[assignment] + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + yield msg diff --git a/faststream/nats/subscriber/usecases/stream_pull_subscriber.py b/faststream/nats/subscriber/usecases/stream_pull_subscriber.py new file mode 100644 index 0000000000..c5375b23c3 --- /dev/null +++ b/faststream/nats/subscriber/usecases/stream_pull_subscriber.py @@ -0,0 +1,237 @@ +from collections.abc import AsyncIterator, Awaitable, Callable +from contextlib import suppress +from typing import ( + TYPE_CHECKING, + Optional, + cast, +) + +import anyio +from nats.errors import ConnectionClosedError, TimeoutError +from typing_extensions import override + +from faststream._internal.endpoint.subscriber.mixins import ConcurrentMixin, TasksMixin +from faststream._internal.endpoint.utils import process_msg +from faststream.nats.parser import ( + BatchParser, +) + +from .basic import DefaultSubscriber +from .stream_basic import StreamSubscriber + +if TYPE_CHECKING: + from nats.aio.msg import Msg + from nats.js import JetStreamContext + + from faststream._internal.basic_types import SendableMessage + from faststream._internal.endpoint.subscriber.call_item import CallsCollection + from faststream.nats.message import NatsMessage + from faststream.nats.schemas import JStream, PullSub + from faststream.nats.subscriber.config import ( + NatsSubscriberConfig, + NatsSubscriberSpecificationConfig, + ) + + +class PullStreamSubscriber( + TasksMixin, + StreamSubscriber, +): + subscription: Optional["JetStreamContext.PullSubscription"] + + def __init__( + self, + config: "NatsSubscriberConfig", + specification: "NatsSubscriberSpecificationConfig", + calls: "CallsCollection", + *, + queue: str, + pull_sub: "PullSub", + stream: "JStream", + ) -> None: + super().__init__( + config, specification, calls, + # basic args + queue=queue, + stream=stream, + ) + + self.pull_sub = pull_sub + + @override + async def _create_subscription(self) -> None: + """Create NATS subscription and start consume task.""" + if self.subscription: + return + + self.subscription = await self.jetstream.pull_subscribe( + subject=self.clear_subject, + config=self.config, + **self.extra_options, + ) + self.add_task(self._consume_pull(cb=self.consume)) + + async def _consume_pull( + self, + cb: Callable[["Msg"], Awaitable["SendableMessage"]], + ) -> None: + """Endless task consuming messages using NATS Pull subscriber.""" + assert self.subscription # nosec B101 + + while self.running: # pragma: no branch + messages = [] + with suppress(TimeoutError, ConnectionClosedError): + messages = await self.subscription.fetch( + batch=self.pull_sub.batch_size, + timeout=self.pull_sub.timeout, + ) + + if messages: + async with anyio.create_task_group() as tg: + for msg in messages: + tg.start_soon(cb, msg) + + +class ConcurrentPullStreamSubscriber(ConcurrentMixin["Msg"], PullStreamSubscriber): + @override + async def _create_subscription(self) -> None: + """Create NATS subscription and start consume task.""" + if self.subscription: + return + + self.start_consume_task() + + self.subscription = await self.jetstream.pull_subscribe( + subject=self.clear_subject, + config=self.config, + **self.extra_options, + ) + self.add_task(self._consume_pull(cb=self._put_msg)) + + +class BatchPullStreamSubscriber( + TasksMixin, + DefaultSubscriber[list["Msg"]], +): + """Batch-message consumer class.""" + + subscription: Optional["JetStreamContext.PullSubscription"] + _fetch_sub: Optional["JetStreamContext.PullSubscription"] + + def __init__( + self, + config: "NatsSubscriberConfig", + specification: "NatsSubscriberSpecificationConfig", + calls: "CallsCollection", + *, + stream: "JStream", + pull_sub: "PullSub", + ) -> None: + parser = BatchParser(pattern=config.subject) + config.decoder = parser.decode_batch + config.parser = parser.parse_batch + super().__init__(config, specification, calls) + + self.stream = stream + self.pull_sub = pull_sub + + @override + async def get_one( + self, + *, + timeout: float = 5, + ) -> Optional["NatsMessage"]: + assert ( # nosec B101 + not self.calls + ), "You can't use `get_one` method if subscriber has registered handlers." + + if not self._fetch_sub: + fetch_sub = self._fetch_sub = await self.jetstream.pull_subscribe( + subject=self.clear_subject, + config=self.config, + **self.extra_options, + ) + else: + fetch_sub = self._fetch_sub + + try: + raw_message = await fetch_sub.fetch( + batch=1, + timeout=timeout, + ) + except TimeoutError: + return None + + context = self._outer_config.fd_config.context + + return cast( + "NatsMessage", + await process_msg( + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ), + ) + + @override + async def __aiter__(self) -> AsyncIterator["NatsMessage"]: # type: ignore[override] + assert ( # nosec B101 + not self.calls + ), "You can't use iterator if subscriber has registered handlers." + + if not self._fetch_sub: + fetch_sub = self._fetch_sub = await self.jetstream.pull_subscribe( + subject=self.clear_subject, + config=self.config, + **self.extra_options, + ) + else: + fetch_sub = self._fetch_sub + + while True: + raw_message = await fetch_sub.fetch(batch=1) + + context = self._outer_config.fd_config.context + + yield cast( + "NatsMessage", + await process_msg( + msg=raw_message, + middlewares=( + m(raw_message, context=context) + for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ), + ) + + @override + async def _create_subscription(self) -> None: + """Create NATS subscription and start consume task.""" + if self.subscription: + return + + self.subscription = await self.jetstream.pull_subscribe( + subject=self.clear_subject, + config=self.config, + **self.extra_options, + ) + self.add_task(self._consume_pull()) + + async def _consume_pull(self) -> None: + """Endless task consuming messages using NATS Pull subscriber.""" + assert self.subscription, "You should call `create_subscription` at first." # nosec B101 + + while self.running: # pragma: no branch + with suppress(TimeoutError, ConnectionClosedError): + messages = await self.subscription.fetch( + batch=self.pull_sub.batch_size, + timeout=self.pull_sub.timeout, + ) + + if messages: + await self.consume(messages) diff --git a/faststream/nats/subscriber/usecases/stream_push_subscriber.py b/faststream/nats/subscriber/usecases/stream_push_subscriber.py new file mode 100644 index 0000000000..fbea271e4f --- /dev/null +++ b/faststream/nats/subscriber/usecases/stream_push_subscriber.py @@ -0,0 +1,51 @@ +from typing import ( + TYPE_CHECKING, + Optional, +) + +from typing_extensions import override + +from faststream._internal.endpoint.subscriber.mixins import ConcurrentMixin + +from .stream_basic import StreamSubscriber + +if TYPE_CHECKING: + from nats.js import JetStreamContext + + +class PushStreamSubscriber(StreamSubscriber): + subscription: Optional["JetStreamContext.PushSubscription"] + + @override + async def _create_subscription(self) -> None: + """Create NATS subscription and start consume task.""" + if self.subscription: + return + + self.subscription = await self.jetstream.subscribe( + subject=self.clear_subject, + queue=self.queue, + cb=self.consume, + config=self.config, + **self.extra_options, + ) + + +class ConcurrentPushStreamSubscriber(ConcurrentMixin["Msg"], StreamSubscriber): + subscription: Optional["JetStreamContext.PushSubscription"] + + @override + async def _create_subscription(self) -> None: + """Create NATS subscription and start consume task.""" + if self.subscription: + return + + self.start_consume_task() + + self.subscription = await self.jetstream.subscribe( + subject=self.clear_subject, + queue=self.queue, + cb=self._put_msg, + config=self.config, + **self.extra_options, + ) diff --git a/faststream/nats/testing.py b/faststream/nats/testing.py index 6d34547d04..4bdade4d5a 100644 --- a/faststream/nats/testing.py +++ b/faststream/nats/testing.py @@ -1,40 +1,57 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +from collections.abc import Generator, Iterable, Iterator +from contextlib import ExitStack, contextmanager +from typing import TYPE_CHECKING, Any, Optional from unittest.mock import AsyncMock import anyio from nats.aio.msg import Msg from typing_extensions import override -from faststream.broker.message import encode_message, gen_cor_id -from faststream.broker.utils import resolve_custom_func -from faststream.exceptions import WRONG_PUBLISH_ARGS, SubscriberNotFound +from faststream._internal.endpoint.utils import resolve_custom_func +from faststream._internal.testing.broker import TestBroker +from faststream.exceptions import SubscriberNotFound +from faststream.message import encode_message, gen_cor_id from faststream.nats.broker import NatsBroker from faststream.nats.parser import NatsParser from faststream.nats.publisher.producer import NatsFastProducer from faststream.nats.schemas.js_stream import is_subject_match_wildcard -from faststream.testing.broker import TestBroker -from faststream.utils.functions import timeout_scope if TYPE_CHECKING: - from faststream.nats.publisher.asyncapi import AsyncAPIPublisher - from faststream.nats.subscriber.usecase import LogicSubscriber - from faststream.types import SendableMessage + from fast_depends.library.serializer import SerializerProto + + from faststream._internal.basic_types import SendableMessage + from faststream._internal.producer import ProducerProto + from faststream.nats.configs import NatsBrokerConfig + from faststream.nats.publisher.specification import SpecificationPublisher + from faststream.nats.response import NatsPublishCommand + from faststream.nats.subscriber.usecases.basic import LogicSubscriber __all__ = ("TestNatsBroker",) +@contextmanager +def change_producer( + config: "NatsBrokerConfig", producer: "ProducerProto" +) -> Generator[None, None, None]: + old_producer, config.broker_config.producer = config.producer, producer + old_js_producer, config.broker_config.js_producer = config.js_producer, producer + yield + config.broker_config.producer = old_producer + config.broker_config.js_producer = old_js_producer + + class TestNatsBroker(TestBroker[NatsBroker]): """A class to test NATS brokers.""" @staticmethod def create_publisher_fake_subscriber( broker: NatsBroker, - publisher: "AsyncAPIPublisher", - ) -> Tuple["LogicSubscriber[Any, Any]", bool]: - sub: Optional[LogicSubscriber[Any, Any]] = None + publisher: "SpecificationPublisher", + ) -> tuple["LogicSubscriber[Any, Any]", bool]: + sub: LogicSubscriber[Any, Any] | None = None publisher_stream = publisher.stream.name if publisher.stream else None - for handler in broker._subscribers.values(): - if _is_handler_suitable(handler, publisher.subject, publisher_stream): + for handler in broker.subscribers: + if _is_handler_matches(handler, publisher.subject, publisher_stream): sub = handler break @@ -46,100 +63,92 @@ def create_publisher_fake_subscriber( return sub, is_real - @staticmethod - async def _fake_connect( # type: ignore[override] + @contextmanager + def _patch_producer(self, broker: NatsBroker) -> Iterator[None]: + fake_producer = FakeProducer(broker) + + with ExitStack() as es: + es.enter_context(change_producer(broker.config, fake_producer)) + yield + + async def _fake_connect( + self, broker: NatsBroker, *args: Any, **kwargs: Any, ) -> AsyncMock: - broker.stream = AsyncMock() - broker._js_producer = broker._producer = FakeProducer( # type: ignore[assignment] - broker, - ) + if not broker.config.connection_state: + broker.config.connection_state.connect(AsyncMock(), AsyncMock()) return AsyncMock() + def _fake_start(self, broker: NatsBroker, *args: Any, **kwargs: Any) -> None: + if not broker.config.connection_state: + broker.config.connection_state.connect(AsyncMock(), AsyncMock()) + return super()._fake_start(broker, *args, **kwargs) + class FakeProducer(NatsFastProducer): def __init__(self, broker: NatsBroker) -> None: self.broker = broker - default = NatsParser(pattern="", no_ack=False) + default = NatsParser(pattern="", is_ack_disabled=True) self._parser = resolve_custom_func(broker._parser, default.parse_message) self._decoder = resolve_custom_func(broker._decoder, default.decode_message) @override async def publish( # type: ignore[override] - self, - message: "SendableMessage", - subject: str, - reply_to: str = "", - headers: Optional[Dict[str, str]] = None, - correlation_id: Optional[str] = None, - # NatsJSFastProducer compatibility - timeout: Optional[float] = None, - stream: Optional[str] = None, - *, - rpc: bool = False, - rpc_timeout: Optional[float] = None, - raise_timeout: bool = False, - ) -> Any: - if rpc and reply_to: - raise WRONG_PUBLISH_ARGS - + self, cmd: "NatsPublishCommand" + ) -> None: incoming = build_message( - message=message, - subject=subject, - headers=headers, - correlation_id=correlation_id, - reply_to=reply_to, + message=cmd.body, + subject=cmd.destination, + headers=cmd.headers, + correlation_id=cmd.correlation_id, + reply_to=cmd.reply_to, + serializer=self.broker.config.fd_config._serializer ) - for handler in self.broker._subscribers.values(): # pragma: no branch - if _is_handler_suitable(handler, subject, stream): - msg: Union[List[PatchedMessage], PatchedMessage] - - if (pull := getattr(handler, "pull_sub", None)) and pull.batch: - msg = [incoming] - else: - msg = incoming + for handler in _find_handler( + self.broker.subscribers, + cmd.destination, + cmd.stream, + ): + msg: list[PatchedMessage] | PatchedMessage - with timeout_scope(rpc_timeout, raise_timeout): - response = await self._execute_handler(msg, subject, handler) - if rpc: - return await self._decoder(await self._parser(response)) + if (pull := getattr(handler, "pull_sub", None)) and pull.batch: + msg = [incoming] + else: + msg = incoming - return None + await self._execute_handler(msg, cmd.destination, handler) @override async def request( # type: ignore[override] self, - message: "SendableMessage", - subject: str, - *, - correlation_id: Optional[str] = None, - headers: Optional[Dict[str, str]] = None, - timeout: float = 0.5, - # NatsJSFastProducer compatibility - stream: Optional[str] = None, + cmd: "NatsPublishCommand", ) -> "PatchedMessage": incoming = build_message( - message=message, - subject=subject, - headers=headers, - correlation_id=correlation_id, + message=cmd.body, + subject=cmd.destination, + headers=cmd.headers, + correlation_id=cmd.correlation_id, + serializer=self.broker.config.fd_config._serializer ) - for handler in self.broker._subscribers.values(): # pragma: no branch - if _is_handler_suitable(handler, subject, stream): - msg: Union[List[PatchedMessage], PatchedMessage] + for handler in _find_handler( + self.broker.subscribers, + cmd.destination, + cmd.stream, + ): + msg: list[PatchedMessage] | PatchedMessage - if (pull := getattr(handler, "pull_sub", None)) and pull.batch: - msg = [incoming] - else: - msg = incoming + if (pull := getattr(handler, "pull_sub", None)) and pull.batch: + msg = [incoming] + else: + msg = incoming - with anyio.fail_after(timeout): - return await self._execute_handler(msg, subject, handler) + with anyio.fail_after(cmd.timeout): + return await self._execute_handler(msg, cmd.destination, handler) raise SubscriberNotFound @@ -156,13 +165,30 @@ async def _execute_handler( message=result.body, headers=result.headers, correlation_id=result.correlation_id, + serializer=self.broker.config.fd_config._serializer ) -def _is_handler_suitable( +def _find_handler( + subscribers: Iterable["LogicSubscriber[Any, Any]"], + subject: str, + stream: str | None = None, +) -> Generator["LogicSubscriber[Any, Any]", None, None]: + published_queues = set() + for handler in subscribers: # pragma: no branch + if _is_handler_matches(handler, subject, stream): + if queue := getattr(handler, "queue", None): + if queue in published_queues: + continue + else: + published_queues.add(queue) + yield handler + + +def _is_handler_matches( handler: "LogicSubscriber[Any, Any]", subject: str, - stream: Optional[str] = None, + stream: str | None = None, ) -> bool: if stream: if not (handler_stream := getattr(handler, "stream", None)): @@ -174,7 +200,7 @@ def _is_handler_suitable( if is_subject_match_wildcard(subject, handler.clear_subject): return True - for filter_subject in handler.config.filter_subjects or (): + for filter_subject in handler.filter_subjects or (): if is_subject_match_wildcard(subject, filter_subject): return True @@ -186,12 +212,13 @@ def build_message( subject: str, *, reply_to: str = "", - correlation_id: Optional[str] = None, - headers: Optional[Dict[str, str]] = None, + correlation_id: str | None = None, + headers: dict[str, str] | None = None, + serializer: Optional["SerializerProto"] = None, ) -> "PatchedMessage": - msg, content_type = encode_message(message) + msg, content_type = encode_message(message, serializer=serializer) return PatchedMessage( - _client=None, # type: ignore + _client=None, # type: ignore[arg-type] subject=subject, reply=reply_to, data=msg, @@ -213,7 +240,7 @@ async def ack_sync( ) -> "PatchedMessage": # pragma: no cover return self - async def nak(self, delay: Union[int, float, None] = None) -> None: + async def nak(self, delay: float | None = None) -> None: pass async def term(self) -> None: diff --git a/faststream/opentelemetry/annotations.py b/faststream/opentelemetry/annotations.py index cdf2378cc3..aeb4fe85f6 100644 --- a/faststream/opentelemetry/annotations.py +++ b/faststream/opentelemetry/annotations.py @@ -1,5 +1,6 @@ +from typing import Annotated + from opentelemetry.trace import Span -from typing_extensions import Annotated from faststream import Context from faststream.opentelemetry.baggage import Baggage diff --git a/faststream/opentelemetry/baggage.py b/faststream/opentelemetry/baggage.py index d225714988..1d475eac98 100644 --- a/faststream/opentelemetry/baggage.py +++ b/faststream/opentelemetry/baggage.py @@ -1,12 +1,12 @@ -from typing import TYPE_CHECKING, Any, List, Optional, cast +from typing import TYPE_CHECKING, Any, Optional, cast from opentelemetry import baggage, context from opentelemetry.baggage.propagation import W3CBaggagePropagator from typing_extensions import Self if TYPE_CHECKING: - from faststream.broker.message import StreamMessage - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict + from faststream.message import StreamMessage _BAGGAGE_PROPAGATOR = W3CBaggagePropagator() @@ -15,7 +15,9 @@ class Baggage: __slots__ = ("_baggage", "_batch_baggage") def __init__( - self, payload: "AnyDict", batch_payload: Optional[List["AnyDict"]] = None + self, + payload: "AnyDict", + batch_payload: list["AnyDict"] | None = None, ) -> None: self._baggage = dict(payload) self._batch_baggage = [dict(b) for b in batch_payload] if batch_payload else [] @@ -24,11 +26,11 @@ def get_all(self) -> "AnyDict": """Get a copy of the current baggage.""" return self._baggage.copy() - def get_all_batch(self) -> List["AnyDict"]: + def get_all_batch(self) -> list["AnyDict"]: """Get a copy of all batch baggage if exists.""" return self._batch_baggage.copy() - def get(self, key: str) -> Optional[Any]: + def get(self, key: str) -> Any | None: """Get a value from the baggage by key.""" return self._baggage.get(key) @@ -60,11 +62,10 @@ def to_headers(self, headers: Optional["AnyDict"] = None) -> "AnyDict": def from_msg(cls, msg: "StreamMessage[Any]") -> Self: """Create a Baggage instance from a StreamMessage.""" if len(msg.batch_headers) <= 1: - payload = baggage.get_all(_BAGGAGE_PROPAGATOR.extract(msg.headers)) - return cls(cast("AnyDict", payload)) + return cls.from_headers(msg.headers) cumulative_baggage: AnyDict = {} - batch_baggage: List[AnyDict] = [] + batch_baggage: list[AnyDict] = [] for headers in msg.batch_headers: payload = baggage.get_all(_BAGGAGE_PROPAGATOR.extract(headers)) diff --git a/faststream/opentelemetry/middleware.py b/faststream/opentelemetry/middleware.py index 2af1a21158..6a257ad188 100644 --- a/faststream/opentelemetry/middleware.py +++ b/faststream/opentelemetry/middleware.py @@ -1,7 +1,8 @@ import time from collections import defaultdict +from collections.abc import Callable from copy import copy -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Type, cast +from typing import TYPE_CHECKING, Any, Generic, Optional, cast from opentelemetry import baggage, context, metrics, trace from opentelemetry.baggage.propagation import W3CBaggagePropagator @@ -10,8 +11,8 @@ from opentelemetry.trace import Link, Span from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -from faststream import BaseMiddleware -from faststream import context as fs_context +from faststream._internal.middlewares import BaseMiddleware +from faststream._internal.types import PublishCommandType from faststream.opentelemetry.baggage import Baggage from faststream.opentelemetry.consts import ( ERROR_TYPE, @@ -22,7 +23,6 @@ WITH_BATCH, MessageAction, ) -from faststream.opentelemetry.provider import TelemetrySettingsProvider if TYPE_CHECKING: from contextvars import Token @@ -32,14 +32,57 @@ from opentelemetry.trace import Tracer, TracerProvider from opentelemetry.util.types import Attributes - from faststream.broker.message import StreamMessage - from faststream.types import AnyDict, AsyncFunc, AsyncFuncAny + from faststream._internal.basic_types import AnyDict, AsyncFunc, AsyncFuncAny + from faststream._internal.context.repository import ContextRepo + from faststream.message import StreamMessage + from faststream.opentelemetry.provider import TelemetrySettingsProvider _BAGGAGE_PROPAGATOR = W3CBaggagePropagator() _TRACE_PROPAGATOR = TraceContextTextMapPropagator() +class TelemetryMiddleware(Generic[PublishCommandType]): + __slots__ = ( + "_meter", + "_metrics", + "_settings_provider_factory", + "_tracer", + ) + + def __init__( + self, + *, + settings_provider_factory: Callable[ + [Any], + Optional["TelemetrySettingsProvider[Any, PublishCommandType]"], + ], + tracer_provider: Optional["TracerProvider"] = None, + meter_provider: Optional["MeterProvider"] = None, + meter: Optional["Meter"] = None, + include_messages_counters: bool = False, + ) -> None: + self._tracer = _get_tracer(tracer_provider) + self._meter = _get_meter(meter_provider, meter) + self._metrics = _MetricsContainer(self._meter, include_messages_counters) + self._settings_provider_factory = settings_provider_factory + + def __call__( + self, + msg: Any | None, + /, + *, + context: "ContextRepo", + ) -> "BaseTelemetryMiddleware[PublishCommandType]": + return BaseTelemetryMiddleware[PublishCommandType]( + msg, + tracer=self._tracer, + metrics_container=self._metrics, + settings_provider_factory=self._settings_provider_factory, + context=context, + ) + + class _MetricsContainer: __slots__ = ( "include_messages_counters", @@ -76,7 +119,10 @@ def __init__(self, meter: "Meter", include_messages_counters: bool) -> None: ) def observe_publish( - self, attrs: "AnyDict", duration: float, msg_count: int + self, + attrs: "AnyDict", + duration: float, + msg_count: int, ) -> None: self.publish_duration.record( amount=duration, @@ -91,7 +137,10 @@ def observe_publish( ) def observe_consume( - self, attrs: "AnyDict", duration: float, msg_count: int + self, + attrs: "AnyDict", + duration: float, + msg_count: int, ) -> None: self.process_duration.record( amount=duration, @@ -106,61 +155,64 @@ def observe_consume( ) -class BaseTelemetryMiddleware(BaseMiddleware): +class BaseTelemetryMiddleware(BaseMiddleware[PublishCommandType]): def __init__( self, + msg: Any | None, + /, *, tracer: "Tracer", settings_provider_factory: Callable[ - [Any], Optional[TelemetrySettingsProvider[Any]] + [Any], + Optional["TelemetrySettingsProvider[Any, PublishCommandType]"], ], metrics_container: _MetricsContainer, - msg: Optional[Any] = None, + context: "ContextRepo", ) -> None: - self.msg = msg + super().__init__(msg, context=context) self._tracer = tracer self._metrics = metrics_container - self._current_span: Optional[Span] = None - self._origin_context: Optional[Context] = None - self._scope_tokens: List[Tuple[str, Token[Any]]] = [] + self._current_span: Span | None = None + self._origin_context: Context | None = None + self._scope_tokens: list[tuple[str, Token[Any]]] = [] self.__settings_provider = settings_provider_factory(msg) async def publish_scope( self, call_next: "AsyncFunc", - msg: Any, - *args: Any, - **kwargs: Any, + msg: "PublishCommandType", ) -> Any: if (provider := self.__settings_provider) is None: - return await call_next(msg, *args, **kwargs) + return await call_next(msg) - headers = kwargs.pop("headers", {}) or {} + headers = msg.headers current_context = context.get_current() - destination_name = provider.get_publish_destination_name(kwargs) + destination_name = provider.get_publish_destination_name(msg) - current_baggage: Optional[Baggage] = fs_context.get_local("baggage") + current_baggage: Baggage | None = self.context.get_local("baggage") if current_baggage: headers.update(current_baggage.to_headers()) - trace_attributes = provider.get_publish_attrs_from_kwargs(kwargs) + trace_attributes = provider.get_publish_attrs_from_cmd(msg) metrics_attributes = { SpanAttributes.MESSAGING_SYSTEM: provider.messaging_system, SpanAttributes.MESSAGING_DESTINATION_NAME: destination_name, } # NOTE: if batch with single message? - if (msg_count := len((msg, *args))) > 1: + if (msg_count := len(msg.batch_bodies)) > 1: trace_attributes[SpanAttributes.MESSAGING_BATCH_MESSAGE_COUNT] = msg_count current_context = _BAGGAGE_PROPAGATOR.extract(headers, current_context) _BAGGAGE_PROPAGATOR.inject( - headers, baggage.set_baggage(WITH_BATCH, True, context=current_context) + headers, + baggage.set_baggage(WITH_BATCH, True, context=current_context), ) if self._current_span and self._current_span.is_recording(): current_context = trace.set_span_in_context( - self._current_span, current_context + self._current_span, + current_context, ) _TRACE_PROPAGATOR.inject(headers, context=self._origin_context) @@ -184,9 +236,11 @@ async def publish_scope( context=current_context, ) as span: span.set_attribute( - SpanAttributes.MESSAGING_OPERATION, MessageAction.PUBLISH + SpanAttributes.MESSAGING_OPERATION, + MessageAction.PUBLISH, ) - result = await call_next(msg, *args, headers=headers, **kwargs) + msg.headers = headers + result = await call_next(msg) except Exception as e: metrics_attributes[ERROR_TYPE] = type(e).__name__ @@ -197,7 +251,7 @@ async def publish_scope( self._metrics.observe_publish(metrics_attributes, duration, msg_count) for key, token in self._scope_tokens: - fs_context.reset_local(key, token) + self.context.reset_local(key, token) return result @@ -245,13 +299,20 @@ async def consume_scope( end_on_exit=False, ) as span: span.set_attribute( - SpanAttributes.MESSAGING_OPERATION, MessageAction.PROCESS + SpanAttributes.MESSAGING_OPERATION, + MessageAction.PROCESS, ) self._current_span = span - self._scope_tokens.append(("span", fs_context.set_local("span", span))) + self._scope_tokens.append(( + "span", + self.context.set_local("span", span), + )) self._scope_tokens.append( - ("baggage", fs_context.set_local("baggage", Baggage.from_msg(msg))) + ( + "baggage", + self.context.set_local("baggage", Baggage.from_msg(msg)), + ), ) new_context = trace.set_span_in_context(span, current_context) @@ -266,7 +327,8 @@ async def consume_scope( finally: duration = time.perf_counter() - start_time msg_count = trace_attributes.get( - SpanAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 1 + SpanAttributes.MESSAGING_BATCH_MESSAGE_COUNT, + 1, ) self._metrics.observe_consume(metrics_attributes, duration, msg_count) @@ -274,49 +336,15 @@ async def consume_scope( async def after_processed( self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, exc_tb: Optional["TracebackType"] = None, - ) -> Optional[bool]: + ) -> bool | None: if self._current_span and self._current_span.is_recording(): self._current_span.end() return False -class TelemetryMiddleware: - # NOTE: should it be class or function? - __slots__ = ( - "_meter", - "_metrics", - "_settings_provider_factory", - "_tracer", - ) - - def __init__( - self, - *, - settings_provider_factory: Callable[ - [Any], Optional[TelemetrySettingsProvider[Any]] - ], - tracer_provider: Optional["TracerProvider"] = None, - meter_provider: Optional["MeterProvider"] = None, - meter: Optional["Meter"] = None, - include_messages_counters: bool = False, - ) -> None: - self._tracer = _get_tracer(tracer_provider) - self._meter = _get_meter(meter_provider, meter) - self._metrics = _MetricsContainer(self._meter, include_messages_counters) - self._settings_provider_factory = settings_provider_factory - - def __call__(self, msg: Optional[Any]) -> BaseMiddleware: - return BaseTelemetryMiddleware( - tracer=self._tracer, - metrics_container=self._metrics, - settings_provider_factory=self._settings_provider_factory, - msg=msg, - ) - - def _get_meter( meter_provider: Optional["MeterProvider"] = None, meter: Optional["Meter"] = None, @@ -345,20 +373,20 @@ def _create_span_name(destination: str, action: str) -> str: def _is_batch_message(msg: "StreamMessage[Any]") -> bool: with_batch = baggage.get_baggage( - WITH_BATCH, _BAGGAGE_PROPAGATOR.extract(msg.headers) + WITH_BATCH, + _BAGGAGE_PROPAGATOR.extract(msg.headers), ) return bool(msg.batch_headers or with_batch) -def _get_msg_links(msg: "StreamMessage[Any]") -> List[Link]: +def _get_msg_links(msg: "StreamMessage[Any]") -> list[Link]: if not msg.batch_headers: if (span := _get_span_from_headers(msg.headers)) is not None: return [Link(span.get_span_context())] - else: - return [] + return [] links = {} - counter: Dict[str, int] = defaultdict(lambda: 0) + counter: dict[str, int] = defaultdict(lambda: 0) for headers in msg.batch_headers: if (correlation_id := headers.get("correlation_id")) is None: @@ -379,13 +407,13 @@ def _get_msg_links(msg: "StreamMessage[Any]") -> List[Link]: return list(links.values()) -def _get_span_from_headers(headers: "AnyDict") -> Optional[Span]: +def _get_span_from_headers(headers: "AnyDict") -> Span | None: trace_context = _TRACE_PROPAGATOR.extract(headers) if not len(trace_context): return None return cast( - "Optional[Span]", + "Span | None", next(iter(trace_context.values())), ) diff --git a/faststream/opentelemetry/provider.py b/faststream/opentelemetry/provider.py index 90232d45ab..0cedf1bd8c 100644 --- a/faststream/opentelemetry/provider.py +++ b/faststream/opentelemetry/provider.py @@ -1,13 +1,24 @@ from typing import TYPE_CHECKING, Protocol -from faststream.broker.types import MsgType +from typing_extensions import TypeVar as TypeVar313 + +from faststream._internal.types import MsgType +from faststream.response import PublishCommand if TYPE_CHECKING: - from faststream.broker.message import StreamMessage - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict + from faststream.message import StreamMessage + + +PublishCommandType_contra = TypeVar313( + "PublishCommandType_contra", + bound=PublishCommand, + default=PublishCommand, + contravariant=True, +) -class TelemetrySettingsProvider(Protocol[MsgType]): +class TelemetrySettingsProvider(Protocol[MsgType, PublishCommandType_contra]): messaging_system: str def get_consume_attrs_from_message( @@ -20,12 +31,12 @@ def get_consume_destination_name( msg: "StreamMessage[MsgType]", ) -> str: ... - def get_publish_attrs_from_kwargs( + def get_publish_attrs_from_cmd( self, - kwargs: "AnyDict", + cmd: PublishCommandType_contra, ) -> "AnyDict": ... def get_publish_destination_name( self, - kwargs: "AnyDict", + cmd: PublishCommandType_contra, ) -> str: ... diff --git a/faststream/params/__init__.py b/faststream/params/__init__.py new file mode 100644 index 0000000000..204cec6df5 --- /dev/null +++ b/faststream/params/__init__.py @@ -0,0 +1,12 @@ +from fast_depends import Depends + +from .no_cast import NoCast +from .params import Context, Header, Path + +__all__ = ( + "Context", + "Depends", + "Header", + "NoCast", + "Path", +) diff --git a/faststream/params/no_cast.py b/faststream/params/no_cast.py new file mode 100644 index 0000000000..282cd267e8 --- /dev/null +++ b/faststream/params/no_cast.py @@ -0,0 +1,27 @@ +from typing import Annotated, Any, TypeVar + +from fast_depends.library import CustomField + +from faststream._internal.basic_types import AnyDict + + +class NoCastField(CustomField): + """A class that represents a custom field without casting. + + You can use it to annotate fields, that should not be casted. + + Usage: + + `data: Annotated[..., NoCast()]` + """ + + def __init__(self) -> None: + super().__init__(cast=False) + + def use(self, **kwargs: Any) -> AnyDict: + return kwargs + + +_NoCastType = TypeVar("_NoCastType") + +NoCast = Annotated[_NoCastType, NoCastField()] diff --git a/faststream/params/params.py b/faststream/params/params.py new file mode 100644 index 0000000000..81005af48a --- /dev/null +++ b/faststream/params/params.py @@ -0,0 +1,48 @@ +from collections.abc import Callable +from typing import Any + +from faststream._internal.constants import EMPTY +from faststream._internal.context import Context as Context_ + + +def Context( # noqa: N802 + real_name: str = "", + *, + cast: bool = False, + default: Any = EMPTY, + initial: Callable[..., Any] | None = None, +) -> Any: + return Context_( + real_name=real_name, + cast=cast, + default=default, + initial=initial, + ) + + +def Header( # noqa: N802 + real_name: str = "", + *, + cast: bool = True, + default: Any = EMPTY, +) -> Any: + return Context_( + real_name=real_name, + cast=cast, + default=default, + prefix="message.headers.", + ) + + +def Path( # noqa: N802 + real_name: str = "", + *, + cast: bool = True, + default: Any = EMPTY, +) -> Any: + return Context_( + real_name=real_name, + cast=cast, + default=default, + prefix="message.path.", + ) diff --git a/faststream/prometheus/__init__.py b/faststream/prometheus/__init__.py index e604b8cef7..a06f158ff3 100644 --- a/faststream/prometheus/__init__.py +++ b/faststream/prometheus/__init__.py @@ -1,9 +1,9 @@ -from faststream.prometheus.middleware import BasePrometheusMiddleware +from faststream.prometheus.middleware import PrometheusMiddleware from faststream.prometheus.provider import MetricsSettingsProvider from faststream.prometheus.types import ConsumeAttrs __all__ = ( - "BasePrometheusMiddleware", "ConsumeAttrs", "MetricsSettingsProvider", + "PrometheusMiddleware", ) diff --git a/faststream/prometheus/consts.py b/faststream/prometheus/consts.py index 3c4648d333..8e592d14ae 100644 --- a/faststream/prometheus/consts.py +++ b/faststream/prometheus/consts.py @@ -1,5 +1,5 @@ -from faststream.broker.message import AckStatus from faststream.exceptions import AckMessage, NackMessage, RejectMessage, SkipMessage +from faststream.message.message import AckStatus from faststream.prometheus.types import ProcessingStatus PROCESSING_STATUS_BY_HANDLER_EXCEPTION_MAP = { @@ -11,7 +11,7 @@ PROCESSING_STATUS_BY_ACK_STATUS = { - AckStatus.acked: ProcessingStatus.acked, - AckStatus.nacked: ProcessingStatus.nacked, - AckStatus.rejected: ProcessingStatus.rejected, + AckStatus.ACKED: ProcessingStatus.acked, + AckStatus.NACKED: ProcessingStatus.nacked, + AckStatus.REJECTED: ProcessingStatus.rejected, } diff --git a/faststream/prometheus/container.py b/faststream/prometheus/container.py index a114f23187..829122cdd5 100644 --- a/faststream/prometheus/container.py +++ b/faststream/prometheus/container.py @@ -1,4 +1,5 @@ -from typing import TYPE_CHECKING, Optional, Sequence, Union, cast +from collections.abc import Sequence +from typing import TYPE_CHECKING, Optional, cast from prometheus_client import Counter, Gauge, Histogram @@ -42,115 +43,128 @@ def __init__( registry: "CollectorRegistry", *, metrics_prefix: str = "faststream", - received_messages_size_buckets: Optional[Sequence[float]] = None, - ): + received_messages_size_buckets: Sequence[float] | None = None, + ) -> None: self._registry = registry self._metrics_prefix = metrics_prefix + received_messages_total_name = f"{metrics_prefix}_received_messages_total" self.received_messages_total = cast( "Counter", - self._get_registered_metric(f"{metrics_prefix}_received_messages_total"), + self._get_registered_metric(received_messages_total_name), ) or Counter( - name=f"{metrics_prefix}_received_messages_total", + name=received_messages_total_name, documentation="Count of received messages by broker and handler", labelnames=["app_name", "broker", "handler"], registry=registry, ) + received_messages_size_bytes_name = ( + f"{metrics_prefix}_received_messages_size_bytes" + ) self.received_messages_size_bytes = cast( "Histogram", - self._get_registered_metric( - f"{metrics_prefix}_received_messages_size_bytes" - ), + self._get_registered_metric(received_messages_size_bytes_name), ) or Histogram( - name=f"{metrics_prefix}_received_messages_size_bytes", + name=received_messages_size_bytes_name, documentation="Histogram of received messages size in bytes by broker and handler", labelnames=["app_name", "broker", "handler"], registry=registry, buckets=received_messages_size_buckets or self.DEFAULT_SIZE_BUCKETS, ) + received_messages_in_process_name = ( + f"{metrics_prefix}_received_messages_in_process" + ) self.received_messages_in_process = cast( "Gauge", - self._get_registered_metric( - f"{metrics_prefix}_received_messages_in_process" - ), + self._get_registered_metric(received_messages_in_process_name), ) or Gauge( - name=f"{metrics_prefix}_received_messages_in_process", + name=received_messages_in_process_name, documentation="Gauge of received messages in process by broker and handler", labelnames=["app_name", "broker", "handler"], registry=registry, ) + received_processed_messages_total_name = ( + f"{metrics_prefix}_received_processed_messages_total" + ) self.received_processed_messages_total = cast( "Counter", - self._get_registered_metric( - f"{metrics_prefix}_received_processed_messages_total" - ), + self._get_registered_metric(received_processed_messages_total_name), ) or Counter( - name=f"{metrics_prefix}_received_processed_messages_total", + name=received_processed_messages_total_name, documentation="Count of received processed messages by broker, handler and status", labelnames=["app_name", "broker", "handler", "status"], registry=registry, ) + received_processed_messages_duration_seconds_name = ( + f"{metrics_prefix}_received_processed_messages_duration_seconds" + ) self.received_processed_messages_duration_seconds = cast( "Histogram", self._get_registered_metric( - f"{metrics_prefix}_received_processed_messages_duration_seconds" + received_processed_messages_duration_seconds_name ), ) or Histogram( - name=f"{metrics_prefix}_received_processed_messages_duration_seconds", + name=received_processed_messages_duration_seconds_name, documentation="Histogram of received processed messages duration in seconds by broker and handler", labelnames=["app_name", "broker", "handler"], registry=registry, ) + received_processed_messages_exceptions_total_name = ( + f"{metrics_prefix}_received_processed_messages_exceptions_total" + ) self.received_processed_messages_exceptions_total = cast( "Counter", self._get_registered_metric( - f"{metrics_prefix}_received_processed_messages_exceptions_total" + received_processed_messages_exceptions_total_name ), ) or Counter( - name=f"{metrics_prefix}_received_processed_messages_exceptions_total", + name=received_processed_messages_exceptions_total_name, documentation="Count of received processed messages exceptions by broker, handler and exception_type", labelnames=["app_name", "broker", "handler", "exception_type"], registry=registry, ) + published_messages_total_name = f"{metrics_prefix}_published_messages_total" self.published_messages_total = cast( "Counter", - self._get_registered_metric(f"{metrics_prefix}_published_messages_total"), + self._get_registered_metric(published_messages_total_name), ) or Counter( - name=f"{metrics_prefix}_published_messages_total", + name=published_messages_total_name, documentation="Count of published messages by destination and status", labelnames=["app_name", "broker", "destination", "status"], registry=registry, ) + published_messages_duration_seconds_name = ( + f"{metrics_prefix}_published_messages_duration_seconds" + ) self.published_messages_duration_seconds = cast( "Histogram", - self._get_registered_metric( - f"{metrics_prefix}_published_messages_duration_seconds" - ), + self._get_registered_metric(published_messages_duration_seconds_name), ) or Histogram( - name=f"{metrics_prefix}_published_messages_duration_seconds", + name=published_messages_duration_seconds_name, documentation="Histogram of published messages duration in seconds by broker and destination", labelnames=["app_name", "broker", "destination"], registry=registry, ) + published_messages_exceptions_total_name = ( + f"{metrics_prefix}_published_messages_exceptions_total" + ) self.published_messages_exceptions_total = cast( "Counter", - self._get_registered_metric( - f"{metrics_prefix}_published_messages_exceptions_total" - ), + self._get_registered_metric(published_messages_exceptions_total_name), ) or Counter( - name=f"{metrics_prefix}_published_messages_exceptions_total", + name=published_messages_exceptions_total_name, documentation="Count of published messages exceptions by broker, destination and exception_type", labelnames=["app_name", "broker", "destination", "exception_type"], registry=registry, ) - def _get_registered_metric(self, metric_name: str) -> Union["Collector", None]: + def _get_registered_metric(self, metric_name: str) -> Optional["Collector"]: return self._registry._names_to_collectors.get(metric_name) diff --git a/faststream/prometheus/manager.py b/faststream/prometheus/manager.py index 10c4211153..583d1424c4 100644 --- a/faststream/prometheus/manager.py +++ b/faststream/prometheus/manager.py @@ -1,11 +1,13 @@ -from faststream.prometheus.container import MetricsContainer -from faststream.prometheus.types import ProcessingStatus, PublishingStatus +from .container import MetricsContainer +from .types import ProcessingStatus, PublishingStatus class MetricsManager: __slots__ = ("_app_name", "_container") - def __init__(self, container: MetricsContainer, *, app_name: str = "faststream"): + def __init__( + self, container: MetricsContainer, *, app_name: str = "faststream" + ) -> None: self._container = container self._app_name = app_name diff --git a/faststream/prometheus/middleware.py b/faststream/prometheus/middleware.py index aabe70285f..03b3a9ead6 100644 --- a/faststream/prometheus/middleware.py +++ b/faststream/prometheus/middleware.py @@ -1,8 +1,12 @@ import time -from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence +from collections.abc import Awaitable, Callable, Sequence +from typing import TYPE_CHECKING, Any, Generic -from faststream import BaseMiddleware +from faststream._internal.constants import EMPTY +from faststream._internal.middlewares import BaseMiddleware +from faststream._internal.types import AnyMsg, PublishCommandType from faststream.exceptions import IgnoredException +from faststream.message import SourceType from faststream.prometheus.consts import ( PROCESSING_STATUS_BY_ACK_STATUS, PROCESSING_STATUS_BY_HANDLER_EXCEPTION_MAP, @@ -11,35 +15,86 @@ from faststream.prometheus.manager import MetricsManager from faststream.prometheus.provider import MetricsSettingsProvider from faststream.prometheus.types import ProcessingStatus, PublishingStatus -from faststream.types import EMPTY +from faststream.response import PublishType if TYPE_CHECKING: from prometheus_client import CollectorRegistry - from faststream.broker.message import StreamMessage - from faststream.types import AsyncFunc, AsyncFuncAny + from faststream._internal.basic_types import AsyncFuncAny + from faststream._internal.context.repository import ContextRepo + from faststream.message.message import StreamMessage -class PrometheusMiddleware(BaseMiddleware): +class PrometheusMiddleware(Generic[PublishCommandType, AnyMsg]): + __slots__ = ("_metrics_container", "_metrics_manager", "_settings_provider_factory") + + def __init__( + self, + *, + settings_provider_factory: Callable[ + [AnyMsg | None], + MetricsSettingsProvider[AnyMsg, PublishCommandType] | None, + ], + registry: "CollectorRegistry", + app_name: str = EMPTY, + metrics_prefix: str = "faststream", + received_messages_size_buckets: Sequence[float] | None = None, + ) -> None: + if app_name is EMPTY: + app_name = metrics_prefix + + self._settings_provider_factory = settings_provider_factory + self._metrics_container = MetricsContainer( + registry, + metrics_prefix=metrics_prefix, + received_messages_size_buckets=received_messages_size_buckets, + ) + self._metrics_manager = MetricsManager( + self._metrics_container, + app_name=app_name, + ) + + def __call__( + self, + msg: AnyMsg | None, + /, + *, + context: "ContextRepo", + ) -> "BasePrometheusMiddleware[PublishCommandType]": + return BasePrometheusMiddleware[PublishCommandType]( + msg, + metrics_manager=self._metrics_manager, + settings_provider_factory=self._settings_provider_factory, + context=context, + ) + + +class BasePrometheusMiddleware( + BaseMiddleware[PublishCommandType, AnyMsg], + Generic[PublishCommandType, AnyMsg], +): def __init__( self, - msg: Optional[Any] = None, + msg: AnyMsg | None, + /, *, settings_provider_factory: Callable[ - [Any], Optional[MetricsSettingsProvider[Any]] + [AnyMsg | None], + MetricsSettingsProvider[AnyMsg, PublishCommandType] | None, ], metrics_manager: MetricsManager, + context: "ContextRepo", ) -> None: self._metrics_manager = metrics_manager self._settings_provider = settings_provider_factory(msg) - super().__init__(msg) + super().__init__(msg, context=context) async def consume_scope( self, call_next: "AsyncFuncAny", - msg: "StreamMessage[Any]", + msg: "StreamMessage[AnyMsg]", ) -> Any: - if self._settings_provider is None: + if self._settings_provider is None or msg._source_type is SourceType.RESPONSE: return await call_next(msg) messaging_system = self._settings_provider.messaging_system @@ -64,7 +119,7 @@ async def consume_scope( handler=destination_name, ) - err: Optional[Exception] = None + err: Exception | None = None start_time = time.perf_counter() try: @@ -115,28 +170,22 @@ async def consume_scope( async def publish_scope( self, - call_next: "AsyncFunc", - msg: Any, - *args: Any, - **kwargs: Any, + call_next: Callable[[PublishCommandType], Awaitable[Any]], + cmd: PublishCommandType, ) -> Any: - if self._settings_provider is None: - return await call_next(msg, *args, **kwargs) + if self._settings_provider is None or cmd.publish_type is PublishType.REPLY: + return await call_next(cmd) destination_name = ( - self._settings_provider.get_publish_destination_name_from_kwargs(kwargs) + self._settings_provider.get_publish_destination_name_from_cmd(cmd) ) messaging_system = self._settings_provider.messaging_system - err: Optional[Exception] = None + err: Exception | None = None start_time = time.perf_counter() try: - result = await call_next( - await self.on_publish(msg, *args, **kwargs), - *args, - **kwargs, - ) + result = await call_next(cmd) except Exception as e: err = e @@ -157,49 +206,12 @@ async def publish_scope( ) status = PublishingStatus.error if err else PublishingStatus.success - messages_count = len((msg, *args)) self._metrics_manager.add_published_message( - amount=messages_count, + amount=len(cmd.batch_bodies), status=status, broker=messaging_system, destination=destination_name, ) return result - - -class BasePrometheusMiddleware: - __slots__ = ("_metrics_container", "_metrics_manager", "_settings_provider_factory") - - def __init__( - self, - *, - settings_provider_factory: Callable[ - [Any], Optional[MetricsSettingsProvider[Any]] - ], - registry: "CollectorRegistry", - app_name: str = EMPTY, - metrics_prefix: str = "faststream", - received_messages_size_buckets: Optional[Sequence[float]] = None, - ): - if app_name is EMPTY: - app_name = metrics_prefix - - self._settings_provider_factory = settings_provider_factory - self._metrics_container = MetricsContainer( - registry, - metrics_prefix=metrics_prefix, - received_messages_size_buckets=received_messages_size_buckets, - ) - self._metrics_manager = MetricsManager( - self._metrics_container, - app_name=app_name, - ) - - def __call__(self, msg: Optional[Any]) -> BaseMiddleware: - return PrometheusMiddleware( - msg=msg, - metrics_manager=self._metrics_manager, - settings_provider_factory=self._settings_provider_factory, - ) diff --git a/faststream/prometheus/provider.py b/faststream/prometheus/provider.py index 1a543f5b55..25013e5306 100644 --- a/faststream/prometheus/provider.py +++ b/faststream/prometheus/provider.py @@ -1,22 +1,32 @@ from typing import TYPE_CHECKING, Protocol -from faststream.broker.message import MsgType +from typing_extensions import TypeVar as TypeVar313 + +from faststream._internal.types import AnyMsg +from faststream.response.response import PublishCommand if TYPE_CHECKING: - from faststream.broker.message import StreamMessage + from faststream.message.message import StreamMessage from faststream.prometheus import ConsumeAttrs - from faststream.types import AnyDict -class MetricsSettingsProvider(Protocol[MsgType]): +PublishCommandType_contra = TypeVar313( + "PublishCommandType_contra", + bound=PublishCommand, + default=PublishCommand, + contravariant=True, +) + + +class MetricsSettingsProvider(Protocol[AnyMsg, PublishCommandType_contra]): messaging_system: str def get_consume_attrs_from_message( self, - msg: "StreamMessage[MsgType]", + msg: "StreamMessage[AnyMsg]", ) -> "ConsumeAttrs": ... - def get_publish_destination_name_from_kwargs( + def get_publish_destination_name_from_cmd( self, - kwargs: "AnyDict", + cmd: PublishCommandType_contra, ) -> str: ... diff --git a/faststream/rabbit/__init__.py b/faststream/rabbit/__init__.py index 8501dadbe6..c3922a267d 100644 --- a/faststream/rabbit/__init__.py +++ b/faststream/rabbit/__init__.py @@ -1,21 +1,22 @@ -try: - from faststream.testing.app import TestApp +from faststream._internal.testing.app import TestApp +try: from .annotations import RabbitMessage - from .broker import RabbitBroker + from .broker import RabbitBroker, RabbitPublisher, RabbitRoute, RabbitRouter from .response import RabbitResponse - from .router import RabbitPublisher, RabbitRoute, RabbitRouter from .schemas import ( Channel, ExchangeType, QueueType, RabbitExchange, RabbitQueue, - ReplyConfig, ) from .testing import TestRabbitBroker except ImportError as e: + if "'aio_pika'" not in e.msg: + raise + from faststream.exceptions import INSTALL_FASTSTREAM_RABBIT raise ImportError(INSTALL_FASTSTREAM_RABBIT) from e @@ -32,7 +33,6 @@ "RabbitResponse", "RabbitRoute", "RabbitRouter", - "ReplyConfig", "TestApp", "TestRabbitBroker", ) diff --git a/faststream/rabbit/annotations.py b/faststream/rabbit/annotations.py index aaa7b3eec2..4a135ecae9 100644 --- a/faststream/rabbit/annotations.py +++ b/faststream/rabbit/annotations.py @@ -1,11 +1,13 @@ +from typing import Annotated + from aio_pika import RobustChannel, RobustConnection -from typing_extensions import Annotated -from faststream.annotations import ContextRepo, Logger, NoCast +from faststream._internal.context import Context +from faststream.annotations import ContextRepo, Logger +from faststream.params import NoCast from faststream.rabbit.broker import RabbitBroker as RB from faststream.rabbit.message import RabbitMessage as RM from faststream.rabbit.publisher.producer import AioPikaFastProducer -from faststream.utils.context import Context __all__ = ( "Channel", @@ -24,10 +26,3 @@ Channel = Annotated[RobustChannel, Context("broker._channel")] Connection = Annotated[RobustConnection, Context("broker._connection")] - -# NOTE: transaction is not for the public usage yet -# async def _get_transaction(connection: Connection) -> RabbitTransaction: -# async with connection.channel(publisher_confirms=False) as channel: -# yield channel.transaction() - -# Transaction = Annotated[RabbitTransaction, Depends(_get_transaction)] diff --git a/faststream/rabbit/broker/__init__.py b/faststream/rabbit/broker/__init__.py index 42c6726006..9b390edaf7 100644 --- a/faststream/rabbit/broker/__init__.py +++ b/faststream/rabbit/broker/__init__.py @@ -1,3 +1,9 @@ -from faststream.rabbit.broker.broker import RabbitBroker +from .broker import RabbitBroker +from .router import RabbitPublisher, RabbitRoute, RabbitRouter -__all__ = ("RabbitBroker",) +__all__ = ( + "RabbitBroker", + "RabbitPublisher", + "RabbitRoute", + "RabbitRouter", +) diff --git a/faststream/rabbit/broker/broker.py b/faststream/rabbit/broker/broker.py index 3484c0eb60..7dcb12f1f9 100644 --- a/faststream/rabbit/broker/broker.py +++ b/faststream/rabbit/broker/broker.py @@ -1,29 +1,30 @@ import logging -import warnings +from collections.abc import Iterable, Sequence from typing import ( TYPE_CHECKING, - Any, - Callable, - Iterable, Optional, - Sequence, - Type, Union, cast, ) from urllib.parse import urlparse import anyio -from aio_pika import connect_robust -from typing_extensions import Annotated, Doc, deprecated, override +from aio_pika import IncomingMessage, RobustConnection, connect_robust +from typing_extensions import override from faststream.__about__ import SERVICE_NAME -from faststream.broker.message import gen_cor_id -from faststream.exceptions import NOT_CONNECTED_YET -from faststream.rabbit.broker.logging import RabbitLoggingBroker -from faststream.rabbit.broker.registrator import RabbitRegistrator -from faststream.rabbit.helpers import ChannelManager, RabbitDeclarer -from faststream.rabbit.publisher.producer import AioPikaFastProducer +from faststream._internal.broker import BrokerUsecase +from faststream._internal.constants import EMPTY +from faststream._internal.di import FastDependsConfig +from faststream.message import gen_cor_id +from faststream.rabbit.configs import RabbitBrokerConfig +from faststream.rabbit.helpers.channel_manager import ChannelManagerImpl +from faststream.rabbit.helpers.declarer import RabbitDeclarerImpl +from faststream.rabbit.publisher.producer import ( + AioPikaFastProducer, + AioPikaFastProducerImpl, +) +from faststream.rabbit.response import RabbitPublishCommand from faststream.rabbit.schemas import ( RABBIT_REPLY, Channel, @@ -31,27 +32,30 @@ RabbitQueue, ) from faststream.rabbit.security import parse_security -from faststream.rabbit.subscriber.asyncapi import AsyncAPISubscriber from faststream.rabbit.utils import build_url -from faststream.types import EMPTY +from faststream.response.publish_type import PublishType +from faststream.specification.schema import BrokerSpec + +from .logging import make_rabbit_logger_state +from .registrator import RabbitRegistrator if TYPE_CHECKING: - from ssl import SSLContext from types import TracebackType + import aiormq from aio_pika import ( - IncomingMessage, RobustChannel, - RobustConnection, RobustExchange, RobustQueue, ) from aio_pika.abc import DateType, HeadersType, SSLOptions, TimeoutType - from fast_depends.dependencies import Depends + from fast_depends.dependencies import Dependant + from fast_depends.library.serializer import SerializerProto from yarl import URL - from faststream.asyncapi import schema as asyncapi - from faststream.broker.types import ( + from faststream._internal.basic_types import LoggerProto + from faststream._internal.broker.abc_broker import Registrator + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, ) @@ -59,19 +63,18 @@ from faststream.rabbit.types import AioPikaSendableMessage from faststream.rabbit.utils import RabbitClientProperties from faststream.security import BaseSecurity - from faststream.types import AnyDict, Decorator, LoggerProto + from faststream.specification.schema.extra import Tag, TagDict class RabbitBroker( RabbitRegistrator, - RabbitLoggingBroker, + BrokerUsecase[IncomingMessage, RobustConnection], ): """A class to represent a RabbitMQ broker.""" url: str - _producer: Optional["AioPikaFastProducer"] - declarer: Optional["RabbitDeclarer"] + _producer: "AioPikaFastProducer" _channel: Optional["RobustChannel"] def __init__( @@ -80,72 +83,36 @@ def __init__( str, "URL", None ] = "amqp://guest:guest@localhost:5672/", # pragma: allowlist secret *, - host: Optional[str] = None, - port: Optional[int] = None, - virtualhost: Optional[str] = None, + host: str | None = None, + port: int | None = None, + virtualhost: str | None = None, ssl_options: Optional["SSLOptions"] = None, client_properties: Optional["RabbitClientProperties"] = None, timeout: "TimeoutType" = None, fail_fast: bool = True, reconnect_interval: "TimeoutType" = 5.0, - default_channel: Optional[Channel] = None, - channel_number: Annotated[ - Optional[int], - deprecated( - "Deprecated in **FastStream 0.5.39**. " - "Please, use `default_channel=Channel(channel_number=...)` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = None, - publisher_confirms: Annotated[ - bool, - deprecated( - "Deprecated in **FastStream 0.5.39**. " - "Please, use `default_channel=Channel(publisher_confirms=...)` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = True, - on_return_raises: Annotated[ - bool, - deprecated( - "Deprecated in **FastStream 0.5.39**. " - "Please, use `default_channel=Channel(on_return_raises=...)` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - max_consumers: Annotated[ - Optional[int], - deprecated( - "Deprecated in **FastStream 0.5.39**. " - "Please, use `default_channel=Channel(prefetch_count=...)` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = None, - app_id: Optional[str] = SERVICE_NAME, - graceful_timeout: Optional[float] = None, + default_channel: Optional["Channel"] = None, + app_id: str | None = SERVICE_NAME, + # broker base args + graceful_timeout: float | None = None, decoder: Optional["CustomCallable"] = None, parser: Optional["CustomCallable"] = None, - dependencies: Iterable["Depends"] = (), + dependencies: Iterable["Dependant"] = (), middlewares: Sequence["BrokerMiddleware[IncomingMessage]"] = (), + routers: Sequence["Registrator[IncomingMessage]"] = (), + # AsyncAPI args security: Optional["BaseSecurity"] = None, - asyncapi_url: Optional[str] = None, - protocol: Optional[str] = None, - protocol_version: Optional[str] = "0.9.1", - description: Optional[str] = None, - tags: Optional[Iterable[Union["asyncapi.Tag", "asyncapi.TagDict"]]] = None, + specification_url: str | None = None, + protocol: str | None = None, + protocol_version: str | None = "0.9.1", + description: str | None = None, + tags: Iterable[Union["Tag", "TagDict"]] = (), + # logging args logger: Optional["LoggerProto"] = EMPTY, log_level: int = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - ] = EMPTY, + # FastDepends args apply_types: bool = True, - validate: bool = True, - _get_dependant: Optional[Callable[..., Any]] = None, - _call_decorators: Iterable["Decorator"] = (), + serializer: Optional["SerializerProto"] = EMPTY, ) -> None: """Initialize the RabbitBroker. @@ -160,30 +127,23 @@ def __init__( fail_fast: Broker startup raises `AMQPConnectionError` if RabbitMQ is unreachable. reconnect_interval: Time to sleep between reconnection attempts. default_channel: Default channel settings to use. - channel_number: Specify the channel number explicitly. Deprecated in **FastStream 0.5.39**. - publisher_confirms: If `True`, the `publish` method will return `bool` type after publish is complete. - Otherwise, it will return `None`. Deprecated in **FastStream 0.5.39**. - on_return_raises: Raise an :class:`aio_pika.exceptions.DeliveryError` when mandatory message will be returned. - Deprecated in **FastStream 0.5.39**. - max_consumers: RabbitMQ channel `qos` / `prefetch_count` option. It limits max messages processing - in the same time count. Deprecated in **FastStream 0.5.39**. app_id: Application name to mark outgoing messages by. graceful_timeout: Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down. decoder: Custom decoder object. parser: Custom parser object. dependencies: Dependencies to apply to all broker subscribers. middlewares: Middlewares to apply to all broker publishers/subscribers. + routers: RabbitRouters to build a broker with. security: Security options to connect broker and generate AsyncAPI server security information. - asyncapi_url: AsyncAPI hardcoded server addresses. Use `servers` if not specified. + specification_url: AsyncAPI hardcoded server addresses. Use `servers` if not specified. protocol: AsyncAPI server protocol. protocol_version: AsyncAPI server protocol version. description: AsyncAPI server description. tags: AsyncAPI server tags. logger: User-specified logger to pass into Context and log service messages. log_level: Service messages log level. - log_fmt: Default logger log format. apply_types: Whether to use FastDepends or not. - validate: Whether to cast types using Pydantic validation. + serializer: FastDepends-compatible serializer to validate incoming messages. _get_dependant: Custom library dependant generator callback. _call_decorators: Any custom decorator to apply to wrapped functions. """ @@ -201,337 +161,134 @@ def __init__( ssl=security_args.get("ssl"), ) - if asyncapi_url is None: - asyncapi_url = str(amqp_url) + if specification_url is None: + specification_url = str(amqp_url) # respect ascynapi_url argument scheme - built_asyncapi_url = urlparse(asyncapi_url) - self.virtual_host = built_asyncapi_url.path + built_asyncapi_url = urlparse(specification_url) if protocol is None: protocol = built_asyncapi_url.scheme - channel_settings = default_channel or Channel( - channel_number=channel_number, - publisher_confirms=publisher_confirms, - on_return_raises=on_return_raises, - prefetch_count=max_consumers, + cm = ChannelManagerImpl(default_channel) + declarer = RabbitDeclarerImpl(cm) + + producer = AioPikaFastProducerImpl( + declarer=declarer, + decoder=decoder, + parser=parser, ) super().__init__( + # connection args url=str(amqp_url), ssl_context=security_args.get("ssl_context"), timeout=timeout, fail_fast=fail_fast, reconnect_interval=reconnect_interval, - # channel args - channel_settings=channel_settings, # Basic args - graceful_timeout=graceful_timeout, - dependencies=dependencies, - decoder=decoder, - parser=parser, - middlewares=middlewares, - # AsyncAPI args - description=description, - asyncapi_url=asyncapi_url, - protocol=protocol or built_asyncapi_url.scheme, - protocol_version=protocol_version, - security=security, - tags=tags, - # Logging args - logger=logger, - log_level=log_level, - log_fmt=log_fmt, - # FastDepends args - apply_types=apply_types, - validate=validate, - _get_dependant=_get_dependant, - _call_decorators=_call_decorators, - ) - - self.app_id = app_id - - self._channel = None - self.declarer = None - - @property - def _subscriber_setup_extra(self) -> "AnyDict": - return { - **super()._subscriber_setup_extra, - "app_id": self.app_id, - "virtual_host": self.virtual_host, - "declarer": self.declarer, - } - - @property - def _publisher_setup_extra(self) -> "AnyDict": - return { - **super()._publisher_setup_extra, - "app_id": self.app_id, - "virtual_host": self.virtual_host, - } - - @override - async def connect( # type: ignore[override] - self, - url: Union[str, "URL", None] = EMPTY, - *, - host: Optional[str] = None, - port: Optional[int] = None, - virtualhost: Optional[str] = None, - ssl_options: Optional["SSLOptions"] = None, - client_properties: Optional["RabbitClientProperties"] = None, - security: Optional["BaseSecurity"] = None, - timeout: "TimeoutType" = None, - fail_fast: bool = EMPTY, - reconnect_interval: "TimeoutType" = EMPTY, - # channel args - default_channel: Optional[Channel] = None, - channel_number: Annotated[ - Optional[int], - deprecated( - "Deprecated in **FastStream 0.5.39**. " - "Please, use `default_channel=Channel(channel_number=...)` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = EMPTY, - publisher_confirms: Annotated[ - bool, - deprecated( - "Deprecated in **FastStream 0.5.39**. " - "Please, use `default_channel=Channel(publisher_confirms=...)` instead. " - "Argument will be removed in **FastStream 0.6.0**." + routers=routers, + config=RabbitBrokerConfig( + channel_manager=cm, + producer=producer, + declarer=declarer, + app_id=app_id, + virtual_host=built_asyncapi_url.path, + # both args + broker_middlewares=middlewares, + broker_parser=parser, + broker_decoder=decoder, + logger=make_rabbit_logger_state( + logger=logger, + log_level=log_level, + ), + fd_config=FastDependsConfig( + use_fastdepends=apply_types, + serializer=serializer, + ), + # subscriber args + broker_dependencies=dependencies, + graceful_timeout=graceful_timeout, + extra_context={ + "broker": self, + }, ), - ] = EMPTY, - on_return_raises: Annotated[ - bool, - deprecated( - "Deprecated in **FastStream 0.5.39**. " - "Please, use `default_channel=Channel(on_return_raises=...)` instead. " - "Argument will be removed in **FastStream 0.6.0**." + specification=BrokerSpec( + description=description, + url=[specification_url], + protocol=protocol or built_asyncapi_url.scheme, + protocol_version=protocol_version, + security=security, + tags=tags, ), - ] = EMPTY, - ) -> "RobustConnection": - """Connect broker object to RabbitMQ. - - To startup subscribers too you should use `broker.start()` after/instead this method. - - Args: - url: RabbitMQ destination location to connect. - host: Destination host. This option overrides `url` option host. - port: Destination port. This option overrides `url` option port. - virtualhost: RabbitMQ virtual host to use in the current broker connection. - ssl_options: Extra ssl options to establish connection. - client_properties: Add custom client capability. - security: Security options to connect broker and generate AsyncAPI server security information. - timeout: Connection establishement timeout. - fail_fast: Broker startup raises `AMQPConnectionError` if RabbitMQ is unreachable. - reconnect_interval: Time to sleep between reconnection attempts. - default_channel: Default channel settings to use. - channel_number: Specify the channel number explicit. - publisher_confirms: if `True` the `publish` method will - return `bool` type after publish is complete. - Otherwise it will returns `None`. - on_return_raises: raise an :class:`aio_pika.exceptions.DeliveryError` - when mandatory message will be returned - """ - kwargs: AnyDict = {} - - if not default_channel and ( - channel_number is not EMPTY - or publisher_confirms is not EMPTY - or on_return_raises is not EMPTY - ): - default_channel = Channel( - channel_number=None if channel_number is EMPTY else channel_number, - publisher_confirms=True - if publisher_confirms is EMPTY - else publisher_confirms, - on_return_raises=False - if on_return_raises is EMPTY - else on_return_raises, - ) - - if default_channel: - kwargs["channel_settings"] = default_channel - - if timeout: - kwargs["timeout"] = timeout - - if fail_fast is not EMPTY: - kwargs["fail_fast"] = fail_fast - - if reconnect_interval is not EMPTY: - kwargs["reconnect_interval"] = reconnect_interval - - url = None if url is EMPTY else url - - if url or any( - (host, port, virtualhost, ssl_options, client_properties, security) - ): - security_args = parse_security(security) - - kwargs["url"] = build_url( - url, - host=host, - port=port, - virtualhost=virtualhost, - ssl_options=ssl_options, - client_properties=client_properties, - login=security_args.get("login"), - password=security_args.get("password"), - ssl=security_args.get("ssl"), - ) - - if ssl_context := security_args.get("ssl_context"): - kwargs["ssl_context"] = ssl_context - - if kwargs: - warnings.warn( - "`RabbitBroker().connect(...) options were " - "deprecated in **FastStream 0.5.40**. " - "Please, use `RabbitBroker(...)` instead. " - "All these options will be removed in **FastStream 0.6.0**.", - DeprecationWarning, - stacklevel=2, - ) - - connection = await super().connect(**kwargs) + ) - return connection + self._channel = None @override - async def _connect( # type: ignore[override] - self, - url: str, - *, - fail_fast: bool, - reconnect_interval: "TimeoutType", - timeout: "TimeoutType", - ssl_context: Optional["SSLContext"], - channel_settings: Channel, - ) -> "RobustConnection": + async def _connect(self) -> "RobustConnection": connection = cast( "RobustConnection", - await connect_robust( - url, - timeout=timeout, - ssl_context=ssl_context, - reconnect_interval=reconnect_interval, - fail_fast=fail_fast, - ), - ) - - ch_manager = self._channel_manager = ChannelManager( - connection, - default_channel=channel_settings, + await connect_robust(**self._connection_kwargs), ) - declarer = self.declarer = RabbitDeclarer(ch_manager) - - if self._channel is None: # pragma: no branch - self._channel = await ch_manager.get_channel(channel_settings) - await declarer.declare_queue(RABBIT_REPLY) - - self._producer = AioPikaFastProducer( - declarer=declarer, - decoder=self._decoder, - parser=self._parser, - ) - - if qos := channel_settings.prefetch_count: - c = AsyncAPISubscriber.build_log_context( - None, - RabbitQueue(""), - RabbitExchange(""), - ) - self._log(f"Set max consumers to {qos}", extra=c) + if self._channel is None: + self.config.connect(connection) + self._channel = await self.config.channel_manager.get_channel() return connection - async def _close( + async def close( self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, exc_tb: Optional["TracebackType"] = None, ) -> None: + await super().close(exc_type, exc_val, exc_tb) + if self._channel is not None: if not self._channel.is_closed: await self._channel.close() self._channel = None - self.declarer = None - self._producer = None - if self._connection is not None: await self._connection.close() + self._connection = None - await super()._close(exc_type, exc_val, exc_tb) + self.config.disconnect() async def start(self) -> None: """Connect broker to RabbitMQ and startup all subscribers.""" + await self.connect() + await self.declare_queue(RABBIT_REPLY) await super().start() - assert self.declarer, NOT_CONNECTED_YET # nosec B101 - - for publisher in self._publishers.values(): - if publisher.exchange is not None: - await self.declare_exchange(publisher.exchange) - - for subscriber in self._subscribers.values(): - self._log( - f"`{subscriber.call_name}` waiting for messages", - extra=subscriber.get_log_context(None), - ) - await subscriber.start() - @override - async def publish( # type: ignore[override] + async def publish( self, message: "AioPikaSendableMessage" = None, queue: Union["RabbitQueue", str] = "", exchange: Union["RabbitExchange", str, None] = None, *, routing_key: str = "", + # publish options mandatory: bool = True, immediate: bool = False, timeout: "TimeoutType" = None, persist: bool = False, - reply_to: Optional[str] = None, - rpc: Annotated[ - bool, - deprecated( - "Deprecated in **FastStream 0.5.17**. Please, use `request` method instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - rpc_timeout: Annotated[ - Optional[float], - deprecated( - "Deprecated in **FastStream 0.5.17**. Please, use `request` method with `timeout` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = 30.0, - raise_timeout: Annotated[ - bool, - deprecated( - "Deprecated in **FastStream 0.5.17**. `request` always raises TimeoutError instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - # message args - correlation_id: Optional[str] = None, + reply_to: str | None = None, + correlation_id: str | None = None, + # message options headers: Optional["HeadersType"] = None, - content_type: Optional[str] = None, - content_encoding: Optional[str] = None, + content_type: str | None = None, + content_encoding: str | None = None, expiration: Optional["DateType"] = None, - message_id: Optional[str] = None, + message_id: str | None = None, timestamp: Optional["DateType"] = None, - message_type: Optional[str] = None, - user_id: Optional[str] = None, - priority: Optional[int] = None, - ) -> Optional[Any]: + message_type: str | None = None, + user_id: str | None = None, + priority: int | None = None, + ) -> Optional["aiormq.abc.ConfirmationFrameType"]: """Publish message directly. This method allows you to publish message in not AsyncAPI-documented way. You can use it in another frameworks @@ -540,62 +297,73 @@ async def publish( # type: ignore[override] Please, use `@broker.publisher(...)` or `broker.publisher(...).publish(...)` instead in a regular way. Args: - message: Message body to send. - queue: Message routing key to publish with. - exchange: Target exchange to publish message to. - routing_key: Message routing key to publish with. Overrides `queue` option if presented. - mandatory: Client waits for confirmation that the message is placed to some queue. - RabbitMQ returns message to client if there is no suitable queue. - immediate: Client expects that there is consumer ready to take the message to work. - RabbitMQ returns message to client if there is no suitable consumer. - timeout: Send confirmation time from RabbitMQ. - persist: Restore the message on RabbitMQ reboot. - reply_to: Reply message routing key to send with (always sending to default exchange). - rpc: Whether to wait for reply in blocking mode. - rpc_timeout: RPC reply waiting time. - raise_timeout: Whether to raise `TimeoutError` or return `None` at **rpc_timeout**. - correlation_id: Manual message **correlation_id** setter. - **correlation_id** is a useful option to trace messages. - headers: Message headers to store metainformation. - content_type: Message **content-type** header. - Used by application, not core RabbitMQ. Will be set automatically if not specified. - content_encoding: Message body content encoding, e.g. **gzip**. - expiration: Message expiration (lifetime) in seconds (or datetime or timedelta). - message_id: Arbitrary message id. Generated automatically if not present. - timestamp: Message publish timestamp. Generated automatically if not present. - message_type: Application-specific message type, e.g. **orders.created**. - user_id: Publisher connection User ID, validated if set. - priority: The message priority (0 by default). + message: + Message body to send. + queue: + Message routing key to publish with. + exchange: + Target exchange to publish message to. + routing_key: + Message routing key to publish with. Overrides `queue` option if presented. + mandatory: + Client waits for confirmation that the message is placed to some queue. RabbitMQ returns message to client if there is no suitable queue. + immediate: + Client expects that there is consumer ready to take the message to work. RabbitMQ returns message to client if there is no suitable consumer. + timeout: + Send confirmation time from RabbitMQ. + persist: + Restore the message on RabbitMQ reboot. + reply_to: + Reply message routing key to send with (always sending to default exchange). + correlation_id: + Manual message **correlation_id** setter. **correlation_id** is a useful option to trace messages. + headers: + Message headers to store metainformation. + content_type: + Message **content-type** header. Used by application, not core RabbitMQ. Will be set automatically if not specified. + content_encoding: + Message body content encoding, e.g. **gzip**. + expiration: + Message expiration (lifetime) in seconds (or datetime or timedelta). + message_id: + Arbitrary message id. Generated automatically if not present. + timestamp: + Message publish timestamp. Generated automatically if not presented. + message_type: + Application-specific message type, e.g. **orders.created**. + user_id: + Publisher connection User ID, validated if set. + priority: + The message priority (0 by default). + + Returns: + An optional `aiormq.abc.ConfirmationFrameType` representing the confirmation frame if RabbitMQ is configured to send confirmations. """ - routing = routing_key or RabbitQueue.validate(queue).routing - correlation_id = correlation_id or gen_cor_id() - - return await super().publish( + cmd = RabbitPublishCommand( message, - producer=self._producer, - routing_key=routing, - app_id=self.app_id, - exchange=exchange, + routing_key=routing_key or RabbitQueue.validate(queue).routing(), + exchange=RabbitExchange.validate(exchange), + correlation_id=correlation_id or gen_cor_id(), + app_id=self.config.app_id, mandatory=mandatory, immediate=immediate, persist=persist, reply_to=reply_to, headers=headers, - correlation_id=correlation_id, content_type=content_type, content_encoding=content_encoding, expiration=expiration, message_id=message_id, - timestamp=timestamp, message_type=message_type, + timestamp=timestamp, user_id=user_id, timeout=timeout, priority=priority, - rpc=rpc, - rpc_timeout=rpc_timeout, - raise_timeout=raise_timeout, + _publish_type=PublishType.PUBLISH, ) + return await super()._basic_publish(cmd, producer=self._producer) + @override async def request( # type: ignore[override] self, @@ -609,16 +377,16 @@ async def request( # type: ignore[override] timeout: "TimeoutType" = None, persist: bool = False, # message args - correlation_id: Optional[str] = None, + correlation_id: str | None = None, headers: Optional["HeadersType"] = None, - content_type: Optional[str] = None, - content_encoding: Optional[str] = None, + content_type: str | None = None, + content_encoding: str | None = None, expiration: Optional["DateType"] = None, - message_id: Optional[str] = None, + message_id: str | None = None, timestamp: Optional["DateType"] = None, - message_type: Optional[str] = None, - user_id: Optional[str] = None, - priority: Optional[int] = None, + message_type: str | None = None, + user_id: str | None = None, + priority: int | None = None, ) -> "RabbitMessage": """Make a synchronous request to RabbitMQ. @@ -648,16 +416,12 @@ async def request( # type: ignore[override] user_id: Publisher connection User ID, validated if set. priority: The message priority (0 by default). """ - routing = routing_key or RabbitQueue.validate(queue).routing - correlation_id = correlation_id or gen_cor_id() - - msg: RabbitMessage = await super().request( + cmd = RabbitPublishCommand( message, - producer=self._producer, - correlation_id=correlation_id, - routing_key=routing, - app_id=self.app_id, - exchange=exchange, + routing_key=routing_key or RabbitQueue.validate(queue).routing(), + exchange=RabbitExchange.validate(exchange), + correlation_id=correlation_id or gen_cor_id(), + app_id=self.config.app_id, mandatory=mandatory, immediate=immediate, persist=persist, @@ -666,38 +430,27 @@ async def request( # type: ignore[override] content_encoding=content_encoding, expiration=expiration, message_id=message_id, - timestamp=timestamp, message_type=message_type, + timestamp=timestamp, user_id=user_id, timeout=timeout, priority=priority, + _publish_type=PublishType.REQUEST, ) + + msg: RabbitMessage = await super()._basic_request(cmd, producer=self._producer) return msg - async def declare_queue( - self, - queue: Annotated[ - "RabbitQueue", - Doc("Queue object to create."), - ], - ) -> "RobustQueue": + async def declare_queue(self, queue: "RabbitQueue") -> "RobustQueue": """Declares queue object in **RabbitMQ**.""" - assert self.declarer, NOT_CONNECTED_YET # nosec B101 - return await self.declarer.declare_queue(queue) + return await self.config.declarer.declare_queue(queue) - async def declare_exchange( - self, - exchange: Annotated[ - "RabbitExchange", - Doc("Exchange object to create."), - ], - ) -> "RobustExchange": + async def declare_exchange(self, exchange: "RabbitExchange") -> "RobustExchange": """Declares exchange object in **RabbitMQ**.""" - assert self.declarer, NOT_CONNECTED_YET # nosec B101 - return await self.declarer.declare_exchange(exchange) + return await self.config.declarer.declare_exchange(exchange) @override - async def ping(self, timeout: Optional[float]) -> bool: + async def ping(self, timeout: float | None) -> bool: sleep_time = (timeout or 10) / 10 with anyio.move_on_after(timeout) as cancel_scope: diff --git a/faststream/rabbit/broker/logging.py b/faststream/rabbit/broker/logging.py index ba8af79647..6ab67b79ec 100644 --- a/faststream/rabbit/broker/logging.py +++ b/faststream/rabbit/broker/logging.py @@ -1,73 +1,62 @@ -import logging -from typing import TYPE_CHECKING, Any, ClassVar, Optional +from functools import partial +from typing import TYPE_CHECKING -from aio_pika import IncomingMessage, RobustConnection -from typing_extensions import Annotated, deprecated - -from faststream.broker.core.usecase import BrokerUsecase -from faststream.log.logging import get_broker_logger -from faststream.types import EMPTY +from faststream._internal.logger import ( + DefaultLoggerStorage, + make_logger_state, +) +from faststream._internal.logger.logging import get_broker_logger if TYPE_CHECKING: - from faststream.types import LoggerProto + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.context import ContextRepo + +class RabbitParamsStorage(DefaultLoggerStorage): + def __init__(self) -> None: + super().__init__() -class RabbitLoggingBroker(BrokerUsecase[IncomingMessage, RobustConnection]): - """A class that extends the LoggingMixin class and adds additional functionality for logging RabbitMQ related information.""" + self._max_exchange_len = 4 + self._max_queue_len = 4 + + def register_subscriber(self, params: "AnyDict") -> None: + self._max_exchange_len = max( + self._max_exchange_len, + len(params.get("exchange", "")), + ) + self._max_queue_len = max( + self._max_queue_len, + len(params.get("queue", "")), + ) - _max_queue_len: int - _max_exchange_len: int - __max_msg_id_ln: ClassVar[int] = 10 + def get_logger(self, *, context: "ContextRepo") -> "LoggerProto": + # TODO: generate unique logger names to not share between brokers + if not (lg := self._get_logger_ref()): + message_id_ln = 10 - def __init__( - self, - *args: Any, - logger: Optional["LoggerProto"] = EMPTY, - log_level: int = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - ] = EMPTY, - **kwargs: Any, - ) -> None: - super().__init__( - *args, - logger=logger, - # TODO: generate unique logger names to not share between brokers - default_logger=get_broker_logger( + lg = get_broker_logger( name="rabbit", default_context={ "queue": "", "exchange": "", }, - message_id_ln=self.__max_msg_id_ln, - ), - log_level=log_level, - log_fmt=log_fmt, - **kwargs, - ) - - self._max_queue_len = 4 - self._max_exchange_len = 4 - - def get_fmt(self) -> str: - return ( - "%(asctime)s %(levelname)-8s - " - f"%(exchange)-{self._max_exchange_len}s | " - f"%(queue)-{self._max_queue_len}s | " - f"%(message_id)-{self.__max_msg_id_ln}s " - "- %(message)s" - ) - - def _setup_log_context( - self, - *, - queue: Optional[str] = None, - exchange: Optional[str] = None, - ) -> None: - """Set up log context.""" - self._max_exchange_len = max(self._max_exchange_len, len(exchange or "")) - self._max_queue_len = max(self._max_queue_len, len(queue or "")) + message_id_ln=message_id_ln, + fmt=( + "%(asctime)s %(levelname)-8s - " + f"%(exchange)-{self._max_exchange_len}s | " + f"%(queue)-{self._max_queue_len}s | " + f"%(message_id)-{message_id_ln}s " + "- %(message)s" + ), + context=context, + log_level=self.logger_log_level, + ) + self._logger_ref.add(lg) + + return lg + + +make_rabbit_logger_state = partial( + make_logger_state, + default_storage_cls=RabbitParamsStorage, +) diff --git a/faststream/rabbit/broker/registrator.py b/faststream/rabbit/broker/registrator.py index be29ed7a6a..f88f854f2e 100644 --- a/faststream/rabbit/broker/registrator.py +++ b/faststream/rabbit/broker/registrator.py @@ -1,41 +1,45 @@ -from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Sequence, Union, cast +from collections.abc import Iterable, Sequence +from typing import TYPE_CHECKING, Any, Optional, Union, cast -from typing_extensions import Annotated, deprecated, override +from typing_extensions import override -from faststream.broker.core.abc import ABCBroker -from faststream.broker.utils import default_filter +from faststream._internal.broker.abc_broker import Registrator +from faststream._internal.constants import EMPTY from faststream.exceptions import SetupError -from faststream.rabbit.publisher.asyncapi import AsyncAPIPublisher -from faststream.rabbit.publisher.usecase import PublishKwargs +from faststream.middlewares import AckPolicy +from faststream.rabbit.publisher.factory import create_publisher +from faststream.rabbit.publisher.options import PublishKwargs from faststream.rabbit.schemas import ( + Channel, RabbitExchange, RabbitQueue, ) -from faststream.rabbit.subscriber.asyncapi import AsyncAPISubscriber from faststream.rabbit.subscriber.factory import create_subscriber if TYPE_CHECKING: from aio_pika import IncomingMessage from aio_pika.abc import DateType, HeadersType, TimeoutType - from fast_depends.dependencies import Depends + from fast_depends.dependencies import Dependant - from faststream.broker.types import ( + from faststream._internal.basic_types import AnyDict + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, - Filter, PublisherMiddleware, SubscriberMiddleware, ) + from faststream.rabbit.configs import RabbitBrokerConfig from faststream.rabbit.message import RabbitMessage - from faststream.rabbit.schemas import Channel, ReplyConfig - from faststream.types import AnyDict + from faststream.rabbit.publisher import RabbitPublisher + from faststream.rabbit.subscriber import RabbitSubscriber -class RabbitRegistrator(ABCBroker["IncomingMessage"]): +class RabbitRegistrator(Registrator["IncomingMessage"]): """Includable to RabbitBroker router.""" - _subscribers: Dict[int, "AsyncAPISubscriber"] - _publishers: Dict[int, "AsyncAPIPublisher"] + config: "RabbitBrokerConfig" + _subscribers: list["RabbitSubscriber"] + _publishers: list["RabbitPublisher"] @override def subscriber( # type: ignore[override] @@ -43,94 +47,66 @@ def subscriber( # type: ignore[override] queue: Union[str, "RabbitQueue"], exchange: Union[str, "RabbitExchange", None] = None, *, + channel: Optional["Channel"] = None, consume_args: Optional["AnyDict"] = None, - dependencies: Iterable["Depends"] = (), + no_ack: bool = EMPTY, + ack_policy: AckPolicy = EMPTY, + # broker arguments + dependencies: Iterable["Dependant"] = (), parser: Optional["CustomCallable"] = None, decoder: Optional["CustomCallable"] = None, middlewares: Sequence["SubscriberMiddleware[RabbitMessage]"] = (), - channel: Optional["Channel"] = None, - reply_config: Annotated[ - Optional["ReplyConfig"], - deprecated( - "Deprecated in **FastStream 0.5.16**. " - "Please, use `RabbitResponse` object as a handler return instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = None, - filter: Annotated[ - "Filter[RabbitMessage]", - deprecated( - "Deprecated in **FastStream 0.5.0**. Please, create `subscriber` object " - "and use it explicitly instead. Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - Union[bool, int], - deprecated( - "Deprecated in **FastStream 0.5.40**." - "Please, manage acknowledgement policy manually." - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - no_ack: bool = False, no_reply: bool = False, - title: Optional[str] = None, - description: Optional[str] = None, + # AsyncAPI information + title: str | None = None, + description: str | None = None, include_in_schema: bool = True, - ) -> AsyncAPISubscriber: - """Declares RabbitMQ subscriber object and binds it to the exchange. - - You can use it as a handler decorator - `@broker.subscriber(...)`. - Or you can create a subscriber object to call it lately - `broker.subscriber(...)`. + ) -> "RabbitSubscriber": + """Subscribe a handler to a RabbitMQ queue. Args: - queue: RabbitMQ queue to listen. **FastStream** declares and binds - queue object to `exchange` automatically if it is not passive (by default). - exchange: RabbitMQ exchange to bind queue to. Uses default exchange - if not present. **FastStream** declares exchange object automatically - if it is not passive (by default). - consume_args: Extra consumer arguments to use in `queue.consume(...)` method. - channel: Channel to use for consuming messages. If not specified, a default channel will be used. - reply_config: Extra options to use at replies publishing. - dependencies: Dependencies list (`[Depends(),]`) to apply to the subscriber. - parser: Parser to map original **IncomingMessage** Msg to FastStream one. - decoder: Function to decode FastStream msg bytes body to python objects. - middlewares: Subscriber middlewares to wrap incoming message processing. - filter: Overload subscriber to consume various messages from the same source. - retry: Whether to `nack` message at processing exception. - no_ack: Whether to disable **FastStream** autoacknowledgement logic or not. - no_reply: Whether to disable **FastStream** RPC and Reply To auto responses or not. - title: AsyncAPI subscriber object title. - description: AsyncAPI subscriber object description. Uses decorated docstring as default. - include_in_schema: Whether to include operation in AsyncAPI schema or not. + queue (Union[str, RabbitQueue]): RabbitMQ queue to listen. **FastStream** declares and binds queue object to `exchange` automatically by default. + exchange (Union[str, RabbitExchange, None], optional): RabbitMQ exchange to bind queue to. Uses default exchange if not presented. **FastStream** declares exchange object automatically by default. + channel (Optional[Channel], optional): Channel to use for consuming messages. + consume_args (Optional[AnyDict], optional): Extra consumer arguments to use in `queue.consume(...)` method. + no_ack (bool, optional): Whether to disable **FastStream** auto acknowledgement logic or not. (Deprecated in 0.6.0, use `ack_policy=AckPolicy.DO_NOTHING` instead. Scheduled to remove in 0.7.0) + ack_policy (AckPolicy, optional): Acknowledgement policy for message processing. + dependencies (Iterable[Dependant], optional): Dependencies list (`[Dependant(),]`) to apply to the subscriber. + parser (Optional[CustomCallable], optional): Parser to map original **IncomingMessage** Msg to FastStream one. + decoder (Optional[CustomCallable], optional): Function to decode FastStream msg bytes body to python objects. + middlewares (Sequence[SubscriberMiddleware[RabbitMessage]], optional): Subscriber middlewares to wrap incoming message processing. (Deprecated in 0.6.0. Use router-level middlewares instead. Scheduled to remove in 0.7.0) + no_reply (bool, optional): Whether to disable **FastStream** RPC and Reply To auto responses or not. + title (Optional[str], optional): AsyncAPI subscriber object title. + description (Optional[str], optional): AsyncAPI subscriber object description. Uses decorated docstring as default. + include_in_schema (bool, optional): Whether to include operation in AsyncAPI schema or not. + + Returns: + RabbitSubscriber: The subscriber specification object. """ - subscriber = cast( - "AsyncAPISubscriber", - super().subscriber( - create_subscriber( - queue=RabbitQueue.validate(queue), - exchange=RabbitExchange.validate(exchange), - consume_args=consume_args, - reply_config=reply_config, - channel=channel, - # subscriber args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=self._middlewares, - broker_dependencies=self._dependencies, - # AsyncAPI - title_=title, - description_=description, - include_in_schema=self._solve_include_in_schema(include_in_schema), - ) - ), + subscriber = create_subscriber( + queue=RabbitQueue.validate(queue), + exchange=RabbitExchange.validate(exchange), + consume_args=consume_args, + channel=channel, + # subscriber args + ack_policy=ack_policy, + no_ack=no_ack, + no_reply=no_reply, + # broker args + config=self.config, + # specification args + title_=title, + description_=description, + include_in_schema=include_in_schema, ) + subscriber = super().subscriber(subscriber) + + subscriber = cast("RabbitSubscriber", subscriber) + return subscriber.add_call( - filter_=filter, - parser_=parser or self._parser, - decoder_=decoder or self._decoder, + parser_=parser, + decoder_=decoder, dependencies_=dependencies, middlewares_=middlewares, ) @@ -146,20 +122,23 @@ def publisher( # type: ignore[override] immediate: bool = False, timeout: "TimeoutType" = None, persist: bool = False, - reply_to: Optional[str] = None, - priority: Optional[int] = None, + reply_to: str | None = None, + priority: int | None = None, + # specific middlewares: Sequence["PublisherMiddleware"] = (), - title: Optional[str] = None, - description: Optional[str] = None, - schema: Optional[Any] = None, + # AsyncAPI information + title: str | None = None, + description: str | None = None, + schema: Any | None = None, include_in_schema: bool = True, + # message args headers: Optional["HeadersType"] = None, - content_type: Optional[str] = None, - content_encoding: Optional[str] = None, + content_type: str | None = None, + content_encoding: str | None = None, expiration: Optional["DateType"] = None, - message_type: Optional[str] = None, - user_id: Optional[str] = None, - ) -> AsyncAPIPublisher: + message_type: str | None = None, + user_id: str | None = None, + ) -> "RabbitPublisher": """Creates long-living and AsyncAPI-documented publisher object. You can use it as a handler decorator (handler should be decorated by `@broker.subscriber(...)` too) - `@broker.publisher(...)`. @@ -210,27 +189,25 @@ def publisher( # type: ignore[override] expiration=expiration, ) - publisher = cast( - "AsyncAPIPublisher", - super().publisher( - AsyncAPIPublisher.create( - routing_key=routing_key, - queue=RabbitQueue.validate(queue), - exchange=RabbitExchange.validate(exchange), - message_kwargs=message_kwargs, - # Specific - broker_middlewares=self._middlewares, - middlewares=middlewares, - # AsyncAPI - title_=title, - description_=description, - schema_=schema, - include_in_schema=self._solve_include_in_schema(include_in_schema), - ) - ), + publisher = create_publisher( + routing_key=routing_key, + queue=RabbitQueue.validate(queue), + exchange=RabbitExchange.validate(exchange), + message_kwargs=message_kwargs, + # publisher args + middlewares=middlewares, + # broker args + config=self.config, + # specification args + title_=title, + description_=description, + schema_=schema, + include_in_schema=include_in_schema, ) - return publisher + publisher = super().publisher(publisher) + + return cast("RabbitPublisher", publisher) @override def include_router( @@ -238,9 +215,9 @@ def include_router( router: "RabbitRegistrator", # type: ignore[override] *, prefix: str = "", - dependencies: Iterable["Depends"] = (), + dependencies: Iterable["Dependant"] = (), middlewares: Iterable["BrokerMiddleware[IncomingMessage]"] = (), - include_in_schema: Optional[bool] = None, + include_in_schema: bool | None = None, ) -> None: if not isinstance(router, RabbitRegistrator): msg = ( diff --git a/faststream/rabbit/broker/router.py b/faststream/rabbit/broker/router.py new file mode 100644 index 0000000000..b0559a62fd --- /dev/null +++ b/faststream/rabbit/broker/router.py @@ -0,0 +1,345 @@ +from collections.abc import Awaitable, Callable, Iterable, Sequence +from typing import TYPE_CHECKING, Annotated, Any, Optional, Union + +from typing_extensions import Doc, deprecated + +from faststream._internal.broker.router import ( + ArgsContainer, + BrokerRouter, + SubscriberRoute, +) +from faststream._internal.constants import EMPTY +from faststream.middlewares import AckPolicy +from faststream.rabbit.configs import RabbitBrokerConfig + +from .registrator import RabbitRegistrator + +if TYPE_CHECKING: + from aio_pika.abc import DateType, HeadersType, TimeoutType + from aio_pika.message import IncomingMessage + from fast_depends.dependencies import Dependant + + from faststream._internal.basic_types import AnyDict + from faststream._internal.broker.abc_broker import Registrator + from faststream._internal.types import ( + BrokerMiddleware, + CustomCallable, + PublisherMiddleware, + SubscriberMiddleware, + ) + from faststream.rabbit.message import RabbitMessage + from faststream.rabbit.schemas import ( + RabbitExchange, + RabbitQueue, + ) + from faststream.rabbit.types import AioPikaSendableMessage + + +class RabbitPublisher(ArgsContainer): + """Delayed RabbitPublisher registration object. + + Just a copy of `RabbitRegistrator.publisher(...)` arguments. + """ + + def __init__( + self, + queue: Annotated[ + Union["RabbitQueue", str], + Doc("Default message routing key to publish with."), + ] = "", + exchange: Annotated[ + Union["RabbitExchange", str, None], + Doc("Target exchange to publish message to."), + ] = None, + *, + routing_key: Annotated[ + str, + Doc( + "Default message routing key to publish with. " + "Overrides `queue` option if presented.", + ), + ] = "", + mandatory: Annotated[ + bool, + Doc( + "Client waits for confirmation that the message is placed to some queue. " + "RabbitMQ returns message to client if there is no suitable queue.", + ), + ] = True, + immediate: Annotated[ + bool, + Doc( + "Client expects that there is consumer ready to take the message to work. " + "RabbitMQ returns message to client if there is no suitable consumer.", + ), + ] = False, + timeout: Annotated[ + "TimeoutType", + Doc("Send confirmation time from RabbitMQ."), + ] = None, + persist: Annotated[ + bool, + Doc("Restore the message on RabbitMQ reboot."), + ] = False, + reply_to: Annotated[ + str | None, + Doc( + "Reply message routing key to send with (always sending to default exchange).", + ), + ] = None, + priority: Annotated[ + int | None, + Doc("The message priority (0 by default)."), + ] = None, + # basic args + middlewares: Annotated[ + Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + Doc("Publisher middlewares to wrap outgoing messages."), + ] = (), + # AsyncAPI args + title: Annotated[ + str | None, + Doc("AsyncAPI publisher object title."), + ] = None, + description: Annotated[ + str | None, + Doc("AsyncAPI publisher object description."), + ] = None, + schema: Annotated[ + Any | None, + Doc( + "AsyncAPI publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = True, + # message args + headers: Annotated[ + Optional["HeadersType"], + Doc( + "Message headers to store metainformation. " + "Can be overridden by `publish.headers` if specified.", + ), + ] = None, + content_type: Annotated[ + str | None, + Doc( + "Message **content-type** header. " + "Used by application, not core RabbitMQ. " + "Will be set automatically if not specified.", + ), + ] = None, + content_encoding: Annotated[ + str | None, + Doc("Message body content encoding, e.g. **gzip**."), + ] = None, + expiration: Annotated[ + Optional["DateType"], + Doc("Message expiration (lifetime) in seconds (or datetime or timedelta)."), + ] = None, + message_type: Annotated[ + str | None, + Doc("Application-specific message type, e.g. **orders.created**."), + ] = None, + user_id: Annotated[ + str | None, + Doc("Publisher connection User ID, validated if set."), + ] = None, + ) -> None: + super().__init__( + queue=queue, + exchange=exchange, + routing_key=routing_key, + mandatory=mandatory, + immediate=immediate, + timeout=timeout, + persist=persist, + reply_to=reply_to, + priority=priority, + headers=headers, + content_type=content_type, + content_encoding=content_encoding, + expiration=expiration, + message_type=message_type, + user_id=user_id, + # basic args + middlewares=middlewares, + # AsyncAPI args + title=title, + description=description, + schema=schema, + include_in_schema=include_in_schema, + ) + + +class RabbitRoute(SubscriberRoute): + """Class to store delayed RabbitBroker subscriber registration. + + Just a copy of `RabbitRegistrator.subscriber(...)` arguments. + """ + + def __init__( + self, + call: Annotated[ + Callable[..., "AioPikaSendableMessage"] | Callable[..., Awaitable["AioPikaSendableMessage"]], + Doc( + "Message handler function " + "to wrap the same with `@broker.subscriber(...)` way.", + ), + ], + queue: Annotated[ + Union[str, "RabbitQueue"], + Doc( + "RabbitMQ queue to listen. " + "**FastStream** declares and binds queue object to `exchange` automatically by default.", + ), + ], + exchange: Annotated[ + Union[str, "RabbitExchange", None], + Doc( + "RabbitMQ exchange to bind queue to. " + "Uses default exchange if not present. " + "**FastStream** declares exchange object automatically by default." + ), + ] = None, + *, + publishers: Annotated[ + Iterable[RabbitPublisher], + Doc("RabbitMQ publishers to broadcast the handler result."), + ] = (), + consume_args: Annotated[ + Optional["AnyDict"], + Doc("Extra consumer arguments to use in `queue.consume(...)` method."), + ] = None, + # broker arguments + dependencies: Annotated[ + Iterable["Dependant"], + Doc("Dependencies list (`[Dependant(),]`) to apply to the subscriber."), + ] = (), + parser: Annotated[ + Optional["CustomCallable"], + Doc("Parser to map original **IncomingMessage** Msg to FastStream one."), + ] = None, + decoder: Annotated[ + Optional["CustomCallable"], + Doc("Function to decode FastStream msg bytes body to python objects."), + ] = None, + middlewares: Annotated[ + Sequence["SubscriberMiddleware[RabbitMessage]"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), + no_ack: Annotated[ + bool, + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), + deprecated( + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" + ), + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: Annotated[ + bool, + Doc( + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", + ), + ] = False, + # AsyncAPI information + title: Annotated[ + str | None, + Doc("AsyncAPI subscriber object title."), + ] = None, + description: Annotated[ + str | None, + Doc( + "AsyncAPI subscriber object description. " + "Uses decorated docstring as default.", + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = True, + ) -> None: + super().__init__( + call, + publishers=publishers, + queue=queue, + exchange=exchange, + consume_args=consume_args, + dependencies=dependencies, + parser=parser, + decoder=decoder, + middlewares=middlewares, + ack_policy=ack_policy, + no_ack=no_ack, + no_reply=no_reply, + title=title, + description=description, + include_in_schema=include_in_schema, + ) + + +class RabbitRouter(RabbitRegistrator, BrokerRouter["IncomingMessage"]): + """Includable to RabbitBroker router.""" + + def __init__( + self, + prefix: Annotated[ + str, + Doc("String prefix to add to all subscribers queues."), + ] = "", + handlers: Annotated[ + Iterable[RabbitRoute], + Doc("Route object to include."), + ] = (), + *, + dependencies: Annotated[ + Iterable["Dependant"], + Doc( + "Dependencies list (`[Dependant(),]`) to apply to all routers' publishers/subscribers.", + ), + ] = (), + middlewares: Annotated[ + Sequence["BrokerMiddleware[IncomingMessage]"], + Doc("Router middlewares to apply to all routers' publishers/subscribers."), + ] = (), + routers: Annotated[ + Sequence["Registrator[IncomingMessage]"], + Doc("Routers to apply to broker."), + ] = (), + parser: Annotated[ + Optional["CustomCallable"], + Doc("Parser to map original **IncomingMessage** Msg to FastStream one."), + ] = None, + decoder: Annotated[ + Optional["CustomCallable"], + Doc("Function to decode FastStream msg bytes body to python objects."), + ] = None, + include_in_schema: Annotated[ + bool | None, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = None, + ) -> None: + super().__init__( + handlers=handlers, + config=RabbitBrokerConfig( + broker_middlewares=middlewares, + broker_dependencies=dependencies, + broker_parser=parser, + broker_decoder=decoder, + include_in_schema=include_in_schema, + prefix=prefix, + ), + routers=routers, + ) diff --git a/faststream/rabbit/configs/__init__.py b/faststream/rabbit/configs/__init__.py new file mode 100644 index 0000000000..b692e68b87 --- /dev/null +++ b/faststream/rabbit/configs/__init__.py @@ -0,0 +1,5 @@ +from .broker import RabbitBrokerConfig + +__all__ = ( + "RabbitBrokerConfig", +) diff --git a/faststream/rabbit/configs/base.py b/faststream/rabbit/configs/base.py new file mode 100644 index 0000000000..04990c80e2 --- /dev/null +++ b/faststream/rabbit/configs/base.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from faststream.rabbit.schemas.exchange import RabbitExchange + from faststream.rabbit.schemas.queue import RabbitQueue + + from .broker import RabbitBrokerConfig + + +@dataclass(kw_only=True) +class RabbitConfig: + queue: "RabbitQueue" + exchange: "RabbitExchange" + + +@dataclass(kw_only=True) +class RabbitEndpointConfig(RabbitConfig): + _outer_config: "RabbitBrokerConfig" diff --git a/faststream/rabbit/configs/broker.py b/faststream/rabbit/configs/broker.py new file mode 100644 index 0000000000..d31aa53477 --- /dev/null +++ b/faststream/rabbit/configs/broker.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from faststream._internal.configs import BrokerConfig +from faststream.rabbit.helpers.channel_manager import FakeChannelManager +from faststream.rabbit.helpers.declarer import FakeRabbitDeclarer +from faststream.rabbit.publisher.producer import FakeAioPikaFastProducer + +if TYPE_CHECKING: + from aio_pika import RobustConnection + + from faststream.rabbit.helpers import ChannelManager, RabbitDeclarer + from faststream.rabbit.publisher.producer import AioPikaFastProducer + + +@dataclass(kw_only=True) +class RabbitBrokerConfig(BrokerConfig): + channel_manager: "ChannelManager" = field(default_factory=FakeChannelManager) + declarer: "RabbitDeclarer" = field(default_factory=FakeRabbitDeclarer) + producer: "AioPikaFastProducer" = field(default_factory=FakeAioPikaFastProducer) + + virtual_host: str = "" + app_id: str | None = None + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(id: {id(self)})" + + def connect(self, connection: "RobustConnection") -> None: + self.channel_manager.connect(connection) + self.producer.connect(serializer=self.fd_config._serializer) + + def disconnect(self) -> None: + self.channel_manager.disconnect() + self.declarer.disconnect() + self.producer.disconnect() diff --git a/faststream/rabbit/fastapi/__init__.py b/faststream/rabbit/fastapi/__init__.py index a46505cc82..cb7c7c26d4 100644 --- a/faststream/rabbit/fastapi/__init__.py +++ b/faststream/rabbit/fastapi/__init__.py @@ -1,11 +1,12 @@ -from typing_extensions import Annotated +from typing import Annotated -from faststream.broker.fastapi.context import Context, ContextRepo, Logger +from faststream._internal.fastapi.context import Context, ContextRepo, Logger from faststream.rabbit.broker import RabbitBroker as RB -from faststream.rabbit.fastapi.router import RabbitRouter from faststream.rabbit.message import RabbitMessage as RM from faststream.rabbit.publisher.producer import AioPikaFastProducer +from .fastapi import RabbitRouter + RabbitMessage = Annotated[RM, Context("message")] RabbitBroker = Annotated[RB, Context("broker")] RabbitProducer = Annotated[AioPikaFastProducer, Context("broker._producer")] diff --git a/faststream/rabbit/fastapi/fastapi.py b/faststream/rabbit/fastapi/fastapi.py new file mode 100644 index 0000000000..07ad5311c7 --- /dev/null +++ b/faststream/rabbit/fastapi/fastapi.py @@ -0,0 +1,803 @@ +import logging +from collections.abc import Callable, Iterable, Sequence +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Optional, + Union, + cast, +) + +from fastapi.datastructures import Default +from fastapi.routing import APIRoute +from fastapi.utils import generate_unique_id +from starlette.responses import JSONResponse +from starlette.routing import BaseRoute +from typing_extensions import Doc, deprecated, override + +from faststream.__about__ import SERVICE_NAME +from faststream._internal.constants import EMPTY +from faststream._internal.fastapi.router import StreamRouter +from faststream.middlewares import AckPolicy +from faststream.rabbit.broker.broker import RabbitBroker as RB +from faststream.rabbit.schemas import ( + RabbitExchange, + RabbitQueue, +) + +if TYPE_CHECKING: + from enum import Enum + + from aio_pika import IncomingMessage + from aio_pika.abc import DateType, HeadersType, SSLOptions, TimeoutType + from fastapi import params + from fastapi.types import IncEx + from pamqp.common import FieldTable + from starlette.responses import Response + from starlette.types import ASGIApp, Lifespan + from yarl import URL + + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.types import ( + BrokerMiddleware, + CustomCallable, + PublisherMiddleware, + SubscriberMiddleware, + ) + from faststream.rabbit.message import RabbitMessage + from faststream.rabbit.publisher import RabbitPublisher + from faststream.rabbit.schemas import Channel + from faststream.rabbit.subscriber import RabbitSubscriber + from faststream.security import BaseSecurity + from faststream.specification.schema.extra import Tag, TagDict + + +class RabbitRouter(StreamRouter["IncomingMessage"]): + """A class to represent a RabbitMQ router for incoming messages.""" + + broker_class = RB + broker: RB + + def __init__( + self, + url: Annotated[ + Union[str, "URL", None], + Doc("RabbitMQ destination location to connect."), + ] = "amqp://guest:guest@localhost:5672/", # pragma: allowlist secret + *, + # connection args + host: Annotated[ + str | None, + Doc("Destination host. This option overrides `url` option host."), + ] = None, + port: Annotated[ + int | None, + Doc("Destination port. This option overrides `url` option port."), + ] = None, + virtualhost: Annotated[ + str | None, + Doc("RabbitMQ virtual host to use in the current broker connection."), + ] = None, + ssl_options: Annotated[ + Optional["SSLOptions"], + Doc("Extra ssl options to establish connection."), + ] = None, + client_properties: Annotated[ + Optional["FieldTable"], + Doc("Add custom client capability."), + ] = None, + timeout: Annotated[ + "TimeoutType", + Doc("Connection establishement timeout."), + ] = None, + fail_fast: Annotated[ + bool, + Doc( + "Broker startup raises `AMQPConnectionError` if RabbitMQ is unreachable.", + ), + ] = True, + reconnect_interval: Annotated[ + "TimeoutType", + Doc("Time to sleep between reconnection attempts."), + ] = 5.0, + # channel args + default_channel: Optional["Channel"] = None, + app_id: Annotated[ + str | None, + Doc("Application name to mark outgoing messages by."), + ] = SERVICE_NAME, + # broker base args + graceful_timeout: Annotated[ + float | None, + Doc( + "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down.", + ), + ] = 15.0, + decoder: Annotated[ + Optional["CustomCallable"], + Doc("Custom decoder object."), + ] = None, + parser: Annotated[ + Optional["CustomCallable"], + Doc("Custom parser object."), + ] = None, + middlewares: Annotated[ + Sequence["BrokerMiddleware[IncomingMessage]"], + Doc("Middlewares to apply to all broker publishers/subscribers."), + ] = (), + # AsyncAPI args + security: Annotated[ + Optional["BaseSecurity"], + Doc( + "Security options to connect broker and generate AsyncAPI server security information.", + ), + ] = None, + specification_url: Annotated[ + str | None, + Doc("AsyncAPI hardcoded server addresses. Use `servers` if not specified."), + ] = None, + protocol: Annotated[ + str | None, + Doc("AsyncAPI server protocol."), + ] = None, + protocol_version: Annotated[ + str | None, + Doc("AsyncAPI server protocol version."), + ] = "0.9.1", + description: Annotated[ + str | None, + Doc("AsyncAPI server description."), + ] = None, + specification_tags: Annotated[ + Iterable[Union["Tag", "TagDict"]], + Doc("AsyncAPI server tags."), + ] = (), + # logging args + logger: Annotated[ + Optional["LoggerProto"], + Doc("User specified logger to pass into Context and log service messages."), + ] = EMPTY, + log_level: Annotated[ + int, + Doc("Service messages log level."), + ] = logging.INFO, + # StreamRouter options + setup_state: Annotated[ + bool, + Doc( + "Whether to add broker to app scope in lifespan. " + "You should disable this option at old ASGI servers.", + ), + ] = True, + schema_url: Annotated[ + str | None, + Doc( + "AsyncAPI schema url. You should set this option to `None` to disable AsyncAPI routes at all.", + ), + ] = "/asyncapi", + # FastAPI args + prefix: Annotated[ + str, + Doc("An optional path prefix for the router."), + ] = "", + tags: Annotated[ + list[Union[str, "Enum"]] | None, + Doc( + """ + A list of tags to be applied to all the *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """, + ), + ] = None, + dependencies: Annotated[ + Sequence["params.Depends"] | None, + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to all the + *path and stream operations* in this router. + + Read more about it in the + [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """, + ), + ] = None, + default_response_class: Annotated[ + type["Response"], + Doc( + """ + The default response class to be used. + + Read more in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). + """, + ), + ] = Default(JSONResponse), + responses: Annotated[ + dict[int | str, "AnyDict"] | None, + Doc( + """ + Additional responses to be shown in OpenAPI. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/). + + And in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """, + ), + ] = None, + callbacks: Annotated[ + list[BaseRoute] | None, + Doc( + """ + OpenAPI callbacks that should apply to all *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """, + ), + ] = None, + routes: Annotated[ + list[BaseRoute] | None, + Doc( + """ + **Note**: you probably shouldn't use this parameter, it is inherited + from Starlette and supported for compatibility. + + --- + + A list of routes to serve incoming HTTP and WebSocket requests. + """, + ), + deprecated( + """ + You normally wouldn't use this parameter with FastAPI, it is inherited + from Starlette and supported for compatibility. + + In FastAPI, you normally would use the *path operation methods*, + like `router.get()`, `router.post()`, etc. + """, + ), + ] = None, + redirect_slashes: Annotated[ + bool, + Doc( + """ + Whether to detect and redirect slashes in URLs when the client doesn't + use the same format. + """, + ), + ] = True, + default: Annotated[ + Optional["ASGIApp"], + Doc( + """ + Default function handler for this router. Used to handle + 404 Not Found errors. + """, + ), + ] = None, + dependency_overrides_provider: Annotated[ + Any | None, + Doc( + """ + Only used internally by FastAPI to handle dependency overrides. + + You shouldn't need to use it. It normally points to the `FastAPI` app + object. + """, + ), + ] = None, + route_class: Annotated[ + type["APIRoute"], + Doc( + """ + Custom route (*path operation*) class to be used by this router. + + Read more about it in the + [FastAPI docs for Custom Request and APIRoute class](https://fastapi.tiangolo.com/how-to/custom-request-and-route/#custom-apiroute-class-in-a-router). + """, + ), + ] = APIRoute, + on_startup: Annotated[ + Sequence[Callable[[], Any]] | None, + Doc( + """ + A list of startup event handler functions. + + You should instead use the `lifespan` handlers. + + Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """, + ), + ] = None, + on_shutdown: Annotated[ + Sequence[Callable[[], Any]] | None, + Doc( + """ + A list of shutdown event handler functions. + + You should instead use the `lifespan` handlers. + + Read more in the + [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """, + ), + ] = None, + lifespan: Annotated[ + Optional["Lifespan[Any]"], + Doc( + """ + A `Lifespan` context manager handler. This replaces `startup` and + `shutdown` functions with a single context manager. + + Read more in the + [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """, + ), + ] = None, + deprecated: Annotated[ + bool | None, + Doc( + """ + Mark all *path operations* in this router as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """, + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) all the *path operations* in this router in the + generated OpenAPI. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """, + ), + ] = True, + generate_unique_id_function: Annotated[ + Callable[["APIRoute"], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """, + ), + ] = Default(generate_unique_id), + ) -> None: + super().__init__( + url, + host=host, + port=port, + virtualhost=virtualhost, + ssl_options=ssl_options, + client_properties=client_properties, + timeout=timeout, + fail_fast=fail_fast, + reconnect_interval=reconnect_interval, + app_id=app_id, + graceful_timeout=graceful_timeout, + decoder=decoder, + parser=parser, + default_channel=default_channel, + middlewares=middlewares, + security=security, + specification_url=specification_url, + protocol=protocol, + protocol_version=protocol_version, + description=description, + logger=logger, + log_level=log_level, + specification_tags=specification_tags, + schema_url=schema_url, + setup_state=setup_state, + # FastAPI kwargs + prefix=prefix, + tags=tags, + dependencies=dependencies, + default_response_class=default_response_class, + responses=responses, + callbacks=callbacks, + routes=routes, + redirect_slashes=redirect_slashes, + default=default, + dependency_overrides_provider=dependency_overrides_provider, + route_class=route_class, + on_startup=on_startup, + on_shutdown=on_shutdown, + deprecated=deprecated, + include_in_schema=include_in_schema, + lifespan=lifespan, + generate_unique_id_function=generate_unique_id_function, + ) + + @override + def subscriber( # type: ignore[override] + self, + queue: Annotated[ + str | RabbitQueue, + Doc( + "RabbitMQ queue to listen. " + "**FastStream** declares and binds queue object to `exchange` automatically by default.", + ), + ], + exchange: Annotated[ + str | RabbitExchange | None, + Doc( + "RabbitMQ exchange to bind queue to. " + "Uses default exchange if not present. " + "**FastStream** declares exchange object automatically by default." + ), + ] = None, + *, + channel: Optional["Channel"] = None, + consume_args: Annotated[ + Optional["AnyDict"], + Doc("Extra consumer arguments to use in `queue.consume(...)` method."), + ] = None, + # broker arguments + dependencies: Annotated[ + Iterable["params.Depends"], + Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), + ] = (), + parser: Annotated[ + Optional["CustomCallable"], + Doc( + "Parser to map original **aio_pika.IncomingMessage** Msg to FastStream one.", + ), + ] = None, + decoder: Annotated[ + Optional["CustomCallable"], + Doc("Function to decode FastStream msg bytes body to python objects."), + ] = None, + middlewares: Annotated[ + Sequence["SubscriberMiddleware[RabbitMessage]"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), + no_ack: Annotated[ + bool, + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), + deprecated( + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" + ), + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: Annotated[ + bool, + Doc( + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", + ), + ] = False, + # AsyncAPI information + title: Annotated[ + str | None, + Doc("AsyncAPI subscriber object title."), + ] = None, + description: Annotated[ + str | None, + Doc( + "AsyncAPI subscriber object description. " + "Uses decorated docstring as default.", + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = True, + # FastAPI args + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """, + ), + ] = Default(None), + response_model_include: Annotated[ + Optional["IncEx"], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """, + ), + ] = None, + response_model_exclude: Annotated[ + Optional["IncEx"], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """, + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """, + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """, + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """, + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """, + ), + ] = False, + ) -> "RabbitSubscriber": + return cast( + "RabbitSubscriber", + super().subscriber( + queue=queue, + exchange=exchange, + consume_args=consume_args, + channel=channel, + dependencies=dependencies, + parser=parser, + decoder=decoder, + middlewares=middlewares, + ack_policy=ack_policy, + no_ack=no_ack, + no_reply=no_reply, + title=title, + description=description, + include_in_schema=include_in_schema, + # FastAPI args + response_model=response_model, + response_model_include=response_model_include, + response_model_exclude=response_model_exclude, + response_model_by_alias=response_model_by_alias, + response_model_exclude_unset=response_model_exclude_unset, + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, + ), + ) + + @override + def publisher( + self, + queue: Annotated[ + RabbitQueue | str, + Doc("Default message routing key to publish with."), + ] = "", + exchange: Annotated[ + RabbitExchange | str | None, + Doc("Target exchange to publish message to."), + ] = None, + *, + routing_key: Annotated[ + str, + Doc( + "Default message routing key to publish with. " + "Overrides `queue` option if presented.", + ), + ] = "", + mandatory: Annotated[ + bool, + Doc( + "Client waits for confirmation that the message is placed to some queue. " + "RabbitMQ returns message to client if there is no suitable queue.", + ), + ] = True, + immediate: Annotated[ + bool, + Doc( + "Client expects that there is consumer ready to take the message to work. " + "RabbitMQ returns message to client if there is no suitable consumer.", + ), + ] = False, + timeout: Annotated[ + "TimeoutType", + Doc("Send confirmation time from RabbitMQ."), + ] = None, + persist: Annotated[ + bool, + Doc("Restore the message on RabbitMQ reboot."), + ] = False, + reply_to: Annotated[ + str | None, + Doc( + "Reply message routing key to send with (always sending to default exchange).", + ), + ] = None, + priority: Annotated[ + int | None, + Doc("The message priority (0 by default)."), + ] = None, + # specific + middlewares: Annotated[ + Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + Doc("Publisher middlewares to wrap outgoing messages."), + ] = (), + # AsyncAPI information + title: Annotated[ + str | None, + Doc("AsyncAPI publisher object title."), + ] = None, + description: Annotated[ + str | None, + Doc("AsyncAPI publisher object description."), + ] = None, + schema: Annotated[ + Any | None, + Doc( + "AsyncAPI publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = True, + # message args + headers: Annotated[ + Optional["HeadersType"], + Doc( + "Message headers to store metainformation. " + "Can be overridden by `publish.headers` if specified.", + ), + ] = None, + content_type: Annotated[ + str | None, + Doc( + "Message **content-type** header. " + "Used by application, not core RabbitMQ. " + "Will be set automatically if not specified.", + ), + ] = None, + content_encoding: Annotated[ + str | None, + Doc("Message body content encoding, e.g. **gzip**."), + ] = None, + expiration: Annotated[ + Optional["DateType"], + Doc("Message expiration (lifetime) in seconds (or datetime or timedelta)."), + ] = None, + message_type: Annotated[ + str | None, + Doc("Application-specific message type, e.g. **orders.created**."), + ] = None, + user_id: Annotated[ + str | None, + Doc("Publisher connection User ID, validated if set."), + ] = None, + ) -> "RabbitPublisher": + return self.broker.publisher( + queue=queue, + exchange=exchange, + routing_key=routing_key, + mandatory=mandatory, + immediate=immediate, + timeout=timeout, + persist=persist, + reply_to=reply_to, + priority=priority, + middlewares=middlewares, + title=title, + description=description, + schema=schema, + include_in_schema=include_in_schema, + headers=headers, + content_type=content_type, + content_encoding=content_encoding, + expiration=expiration, + message_type=message_type, + user_id=user_id, + ) diff --git a/faststream/rabbit/fastapi/router.py b/faststream/rabbit/fastapi/router.py deleted file mode 100644 index 14ad0f52d2..0000000000 --- a/faststream/rabbit/fastapi/router.py +++ /dev/null @@ -1,859 +0,0 @@ -import logging -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, - Optional, - Sequence, - Type, - Union, - cast, -) - -from fastapi.datastructures import Default -from fastapi.routing import APIRoute -from fastapi.utils import generate_unique_id -from starlette.responses import JSONResponse -from starlette.routing import BaseRoute -from typing_extensions import Annotated, Doc, deprecated, override - -from faststream.__about__ import SERVICE_NAME -from faststream.broker.fastapi.router import StreamRouter -from faststream.broker.utils import default_filter -from faststream.rabbit.broker.broker import RabbitBroker as RB -from faststream.rabbit.publisher.asyncapi import AsyncAPIPublisher -from faststream.rabbit.schemas import ( - RabbitExchange, - RabbitQueue, -) -from faststream.rabbit.subscriber.asyncapi import AsyncAPISubscriber -from faststream.types import EMPTY - -if TYPE_CHECKING: - from enum import Enum - - from aio_pika import IncomingMessage - from aio_pika.abc import DateType, HeadersType, SSLOptions, TimeoutType - from fastapi import params - from fastapi.types import IncEx - from pamqp.common import FieldTable - from starlette.responses import Response - from starlette.types import ASGIApp, Lifespan - from yarl import URL - - from faststream.asyncapi import schema as asyncapi - from faststream.broker.types import ( - BrokerMiddleware, - CustomCallable, - Filter, - PublisherMiddleware, - SubscriberMiddleware, - ) - from faststream.rabbit.message import RabbitMessage - from faststream.rabbit.schemas import Channel, ReplyConfig - from faststream.security import BaseSecurity - from faststream.types import AnyDict, LoggerProto - - -class RabbitRouter(StreamRouter["IncomingMessage"]): - """A class to represent a RabbitMQ router for incoming messages.""" - - broker_class = RB - broker: RB - - def __init__( - self, - url: Annotated[ - Union[str, "URL", None], - Doc("RabbitMQ destination location to connect."), - ] = "amqp://guest:guest@localhost:5672/", # pragma: allowlist secret - *, - # connection args - host: Annotated[ - Optional[str], - Doc("Destination host. This option overrides `url` option host."), - ] = None, - port: Annotated[ - Optional[int], - Doc("Destination port. This option overrides `url` option port."), - ] = None, - virtualhost: Annotated[ - Optional[str], - Doc("RabbitMQ virtual host to use in the current broker connection."), - ] = None, - ssl_options: Annotated[ - Optional["SSLOptions"], - Doc("Extra ssl options to establish connection."), - ] = None, - client_properties: Annotated[ - Optional["FieldTable"], - Doc("Add custom client capability."), - ] = None, - timeout: Annotated[ - "TimeoutType", - Doc("Connection establishement timeout."), - ] = None, - fail_fast: Annotated[ - bool, - Doc( - "Broker startup raises `AMQPConnectionError` if RabbitMQ is unreachable." - ), - ] = True, - reconnect_interval: Annotated[ - "TimeoutType", - Doc("Time to sleep between reconnection attempts."), - ] = 5.0, - # channel args - channel_number: Annotated[ - Optional[int], - Doc("Specify the channel number explicit."), - ] = None, - publisher_confirms: Annotated[ - bool, - Doc( - "if `True` the `publish` method will " - "return `bool` type after publish is complete." - "Otherwise it will returns `None`." - ), - ] = True, - on_return_raises: Annotated[ - bool, - Doc( - "raise an :class:`aio_pika.exceptions.DeliveryError`" - "when mandatory message will be returned" - ), - ] = False, - # broker args - max_consumers: Annotated[ - Optional[int], - Doc( - "RabbitMQ channel `qos` option. " - "It limits max messages processing in the same time count." - ), - ] = None, - app_id: Annotated[ - Optional[str], - Doc("Application name to mark outgoing messages by."), - ] = SERVICE_NAME, - # broker base args - graceful_timeout: Annotated[ - Optional[float], - Doc( - "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down." - ), - ] = 15.0, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Custom decoder object."), - ] = None, - parser: Annotated[ - Optional["CustomCallable"], - Doc("Custom parser object."), - ] = None, - middlewares: Annotated[ - Sequence["BrokerMiddleware[IncomingMessage]"], - Doc("Middlewares to apply to all broker publishers/subscribers."), - ] = (), - # AsyncAPI args - security: Annotated[ - Optional["BaseSecurity"], - Doc( - "Security options to connect broker and generate AsyncAPI server security information." - ), - ] = None, - asyncapi_url: Annotated[ - Optional[str], - Doc("AsyncAPI hardcoded server addresses. Use `servers` if not specified."), - ] = None, - protocol: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol."), - ] = None, - protocol_version: Annotated[ - Optional[str], - Doc("AsyncAPI server protocol version."), - ] = "0.9.1", - description: Annotated[ - Optional[str], - Doc("AsyncAPI server description."), - ] = None, - asyncapi_tags: Annotated[ - Optional[Iterable[Union["asyncapi.Tag", "asyncapi.TagDict"]]], - Doc("AsyncAPI server tags."), - ] = None, - # logging args - logger: Annotated[ - Optional["LoggerProto"], - Doc("User specified logger to pass into Context and log service messages."), - ] = EMPTY, - log_level: Annotated[ - int, - Doc("Service messages log level."), - ] = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - Doc("Default logger log format."), - ] = EMPTY, - # StreamRouter options - setup_state: Annotated[ - bool, - Doc( - "Whether to add broker to app scope in lifespan. " - "You should disable this option at old ASGI servers." - ), - ] = True, - schema_url: Annotated[ - Optional[str], - Doc( - "AsyncAPI schema url. You should set this option to `None` to disable AsyncAPI routes at all." - ), - ] = "/asyncapi", - # FastAPI args - prefix: Annotated[ - str, - Doc("An optional path prefix for the router."), - ] = "", - tags: Annotated[ - Optional[List[Union[str, "Enum"]]], - Doc( - """ - A list of tags to be applied to all the *path operations* in this - router. - - It will be added to the generated OpenAPI (e.g. visible at `/docs`). - - Read more about it in the - [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). - """ - ), - ] = None, - dependencies: Annotated[ - Optional[Sequence["params.Depends"]], - Doc( - """ - A list of dependencies (using `Depends()`) to be applied to all the - *path and stream operations* in this router. - - Read more about it in the - [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). - """ - ), - ] = None, - default_response_class: Annotated[ - Type["Response"], - Doc( - """ - The default response class to be used. - - Read more in the - [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). - """ - ), - ] = Default(JSONResponse), - responses: Annotated[ - Optional[Dict[Union[int, str], "AnyDict"]], - Doc( - """ - Additional responses to be shown in OpenAPI. - - It will be added to the generated OpenAPI (e.g. visible at `/docs`). - - Read more about it in the - [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/). - - And in the - [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). - """ - ), - ] = None, - callbacks: Annotated[ - Optional[List[BaseRoute]], - Doc( - """ - OpenAPI callbacks that should apply to all *path operations* in this - router. - - It will be added to the generated OpenAPI (e.g. visible at `/docs`). - - Read more about it in the - [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). - """ - ), - ] = None, - routes: Annotated[ - Optional[List[BaseRoute]], - Doc( - """ - **Note**: you probably shouldn't use this parameter, it is inherited - from Starlette and supported for compatibility. - - --- - - A list of routes to serve incoming HTTP and WebSocket requests. - """ - ), - deprecated( - """ - You normally wouldn't use this parameter with FastAPI, it is inherited - from Starlette and supported for compatibility. - - In FastAPI, you normally would use the *path operation methods*, - like `router.get()`, `router.post()`, etc. - """ - ), - ] = None, - redirect_slashes: Annotated[ - bool, - Doc( - """ - Whether to detect and redirect slashes in URLs when the client doesn't - use the same format. - """ - ), - ] = True, - default: Annotated[ - Optional["ASGIApp"], - Doc( - """ - Default function handler for this router. Used to handle - 404 Not Found errors. - """ - ), - ] = None, - dependency_overrides_provider: Annotated[ - Optional[Any], - Doc( - """ - Only used internally by FastAPI to handle dependency overrides. - - You shouldn't need to use it. It normally points to the `FastAPI` app - object. - """ - ), - ] = None, - route_class: Annotated[ - Type["APIRoute"], - Doc( - """ - Custom route (*path operation*) class to be used by this router. - - Read more about it in the - [FastAPI docs for Custom Request and APIRoute class](https://fastapi.tiangolo.com/how-to/custom-request-and-route/#custom-apiroute-class-in-a-router). - """ - ), - ] = APIRoute, - on_startup: Annotated[ - Optional[Sequence[Callable[[], Any]]], - Doc( - """ - A list of startup event handler functions. - - You should instead use the `lifespan` handlers. - - Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ - ), - ] = None, - on_shutdown: Annotated[ - Optional[Sequence[Callable[[], Any]]], - Doc( - """ - A list of shutdown event handler functions. - - You should instead use the `lifespan` handlers. - - Read more in the - [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ - ), - ] = None, - lifespan: Annotated[ - Optional["Lifespan[Any]"], - Doc( - """ - A `Lifespan` context manager handler. This replaces `startup` and - `shutdown` functions with a single context manager. - - Read more in the - [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ - ), - ] = None, - deprecated: Annotated[ - Optional[bool], - Doc( - """ - Mark all *path operations* in this router as deprecated. - - It will be added to the generated OpenAPI (e.g. visible at `/docs`). - - Read more about it in the - [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). - """ - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc( - """ - To include (or not) all the *path operations* in this router in the - generated OpenAPI. - - This affects the generated OpenAPI (e.g. visible at `/docs`). - - Read more about it in the - [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). - """ - ), - ] = True, - generate_unique_id_function: Annotated[ - Callable[["APIRoute"], str], - Doc( - """ - Customize the function used to generate unique IDs for the *path - operations* shown in the generated OpenAPI. - - This is particularly useful when automatically generating clients or - SDKs for your API. - - Read more about it in the - [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). - """ - ), - ] = Default(generate_unique_id), - ) -> None: - super().__init__( - url, - host=host, - port=port, - virtualhost=virtualhost, - ssl_options=ssl_options, - client_properties=client_properties, - timeout=timeout, - fail_fast=fail_fast, - reconnect_interval=reconnect_interval, - max_consumers=max_consumers, - app_id=app_id, - graceful_timeout=graceful_timeout, - decoder=decoder, - parser=parser, - channel_number=channel_number, - publisher_confirms=publisher_confirms, - on_return_raises=on_return_raises, - middlewares=middlewares, - security=security, - asyncapi_url=asyncapi_url, - protocol=protocol, - protocol_version=protocol_version, - description=description, - logger=logger, - log_level=log_level, - log_fmt=log_fmt, - asyncapi_tags=asyncapi_tags, - schema_url=schema_url, - setup_state=setup_state, - # FastAPI kwargs - prefix=prefix, - tags=tags, - dependencies=dependencies, - default_response_class=default_response_class, - responses=responses, - callbacks=callbacks, - routes=routes, - redirect_slashes=redirect_slashes, - default=default, - dependency_overrides_provider=dependency_overrides_provider, - route_class=route_class, - on_startup=on_startup, - on_shutdown=on_shutdown, - deprecated=deprecated, - include_in_schema=include_in_schema, - lifespan=lifespan, - generate_unique_id_function=generate_unique_id_function, - ) - - @override - def subscriber( # type: ignore[override] - self, - queue: Annotated[ - Union[str, RabbitQueue], - Doc( - "RabbitMQ queue to listen. " - "**FastStream** declares and binds queue object to `exchange` automatically if it is not passive (by default)." - ), - ], - exchange: Annotated[ - Union[str, RabbitExchange, None], - Doc( - "RabbitMQ exchange to bind queue to. " - "Uses default exchange if not present. " - "**FastStream** declares exchange object automatically if it is not passive (by default)." - ), - ] = None, - *, - channel: Optional["Channel"] = None, - consume_args: Annotated[ - Optional["AnyDict"], - Doc("Extra consumer arguments to use in `queue.consume(...)` method."), - ] = None, - reply_config: Annotated[ - Optional["ReplyConfig"], - Doc("Extra options to use at replies publishing."), - deprecated( - "Deprecated in **FastStream 0.5.16**. " - "Please, use `RabbitResponse` object as a handler return instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = None, - # broker arguments - dependencies: Annotated[ - Iterable["params.Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc( - "Parser to map original **aio_pika.IncomingMessage** Msg to FastStream one." - ), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - middlewares: Annotated[ - Iterable["SubscriberMiddleware[RabbitMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[RabbitMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), - deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - Union[bool, int], - Doc("Whether to `nack` message at processing exception."), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI information - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - # FastAPI args - response_model: Annotated[ - Any, - Doc( - """ - The type to use for the response. - - It could be any valid Pydantic *field* type. So, it doesn't have to - be a Pydantic model, it could be other things, like a `list`, `dict`, - etc. - - It will be used for: - - * Documentation: the generated OpenAPI (and the UI at `/docs`) will - show it as the response (JSON Schema). - * Serialization: you could return an arbitrary object and the - `response_model` would be used to serialize that object into the - corresponding JSON. - * Filtering: the JSON sent to the client will only contain the data - (fields) defined in the `response_model`. If you returned an object - that contains an attribute `password` but the `response_model` does - not include that field, the JSON sent to the client would not have - that `password`. - * Validation: whatever you return will be serialized with the - `response_model`, converting any data as necessary to generate the - corresponding JSON. But if the data in the object returned is not - valid, that would mean a violation of the contract with the client, - so it's an error from the API developer. So, FastAPI will raise an - error and return a 500 error code (Internal Server Error). - - Read more about it in the - [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). - """ - ), - ] = Default(None), - response_model_include: Annotated[ - Optional["IncEx"], - Doc( - """ - Configuration passed to Pydantic to include only certain fields in the - response data. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = None, - response_model_exclude: Annotated[ - Optional["IncEx"], - Doc( - """ - Configuration passed to Pydantic to exclude certain fields in the - response data. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = None, - response_model_by_alias: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response model - should be serialized by alias when an alias is used. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ - ), - ] = True, - response_model_exclude_unset: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data - should have all the fields, including the ones that were not set and - have their default values. This is different from - `response_model_exclude_defaults` in that if the fields are set, - they will be included in the response, even if the value is the same - as the default. - - When `True`, default values are omitted from the response. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ - ), - ] = False, - response_model_exclude_defaults: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data - should have all the fields, including the ones that have the same value - as the default. This is different from `response_model_exclude_unset` - in that if the fields are set but contain the same default values, - they will be excluded from the response. - - When `True`, default values are omitted from the response. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ - ), - ] = False, - response_model_exclude_none: Annotated[ - bool, - Doc( - """ - Configuration passed to Pydantic to define if the response data should - exclude fields set to `None`. - - This is much simpler (less smart) than `response_model_exclude_unset` - and `response_model_exclude_defaults`. You probably want to use one of - those two instead of this one, as those allow returning `None` values - when it makes sense. - - Read more about it in the - [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). - """ - ), - ] = False, - ) -> AsyncAPISubscriber: - return cast( - "AsyncAPISubscriber", - super().subscriber( - queue=queue, - exchange=exchange, - consume_args=consume_args, - reply_config=reply_config, - channel=channel, - dependencies=dependencies, - parser=parser, - decoder=decoder, - middlewares=middlewares, - filter=filter, - retry=retry, - no_ack=no_ack, - no_reply=no_reply, - title=title, - description=description, - include_in_schema=include_in_schema, - # FastAPI args - response_model=response_model, - response_model_include=response_model_include, - response_model_exclude=response_model_exclude, - response_model_by_alias=response_model_by_alias, - response_model_exclude_unset=response_model_exclude_unset, - response_model_exclude_defaults=response_model_exclude_defaults, - response_model_exclude_none=response_model_exclude_none, - ), - ) - - @override - def publisher( - self, - queue: Annotated[ - Union[RabbitQueue, str], - Doc("Default message routing key to publish with."), - ] = "", - exchange: Annotated[ - Union[RabbitExchange, str, None], - Doc("Target exchange to publish message to."), - ] = None, - *, - routing_key: Annotated[ - str, - Doc( - "Default message routing key to publish with. " - "Overrides `queue` option if presented." - ), - ] = "", - mandatory: Annotated[ - bool, - Doc( - "Client waits for confirmation that the message is placed to some queue. " - "RabbitMQ returns message to client if there is no suitable queue." - ), - ] = True, - immediate: Annotated[ - bool, - Doc( - "Client expects that there is consumer ready to take the message to work. " - "RabbitMQ returns message to client if there is no suitable consumer." - ), - ] = False, - timeout: Annotated[ - "TimeoutType", - Doc("Send confirmation time from RabbitMQ."), - ] = None, - persist: Annotated[ - bool, - Doc("Restore the message on RabbitMQ reboot."), - ] = False, - reply_to: Annotated[ - Optional[str], - Doc( - "Reply message routing key to send with (always sending to default exchange)." - ), - ] = None, - priority: Annotated[ - Optional[int], - Doc("The message priority (0 by default)."), - ] = None, - # specific - middlewares: Annotated[ - Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI information - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - # message args - headers: Annotated[ - Optional["HeadersType"], - Doc( - "Message headers to store metainformation. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - content_type: Annotated[ - Optional[str], - Doc( - "Message **content-type** header. " - "Used by application, not core RabbitMQ. " - "Will be set automatically if not specified." - ), - ] = None, - content_encoding: Annotated[ - Optional[str], - Doc("Message body content encoding, e.g. **gzip**."), - ] = None, - expiration: Annotated[ - Optional["DateType"], - Doc("Message expiration (lifetime) in seconds (or datetime or timedelta)."), - ] = None, - message_type: Annotated[ - Optional[str], - Doc("Application-specific message type, e.g. **orders.created**."), - ] = None, - user_id: Annotated[ - Optional[str], - Doc("Publisher connection User ID, validated if set."), - ] = None, - ) -> AsyncAPIPublisher: - return self.broker.publisher( - queue=queue, - exchange=exchange, - routing_key=routing_key, - mandatory=mandatory, - immediate=immediate, - timeout=timeout, - persist=persist, - reply_to=reply_to, - priority=priority, - middlewares=middlewares, - title=title, - description=description, - schema=schema, - include_in_schema=include_in_schema, - headers=headers, - content_type=content_type, - content_encoding=content_encoding, - expiration=expiration, - message_type=message_type, - user_id=user_id, - ) diff --git a/faststream/rabbit/helpers/channel_manager.py b/faststream/rabbit/helpers/channel_manager.py index 7a34cd200c..debb2b6cc0 100644 --- a/faststream/rabbit/helpers/channel_manager.py +++ b/faststream/rabbit/helpers/channel_manager.py @@ -1,36 +1,71 @@ -from typing import TYPE_CHECKING, Dict, Optional, cast +from typing import TYPE_CHECKING, Optional, Protocol, cast + +from faststream.rabbit.schemas import Channel + +from .state import ConnectedState, ConnectionState, EmptyConnectionState if TYPE_CHECKING: import aio_pika - from faststream.rabbit.schemas import Channel +class ChannelManager(Protocol): + def connect(self, connection: "aio_pika.RobustConnection") -> None: ... + + def disconnect(self) -> None: ... + + async def get_channel( + self, + channel: Optional["Channel"] = None, + ) -> "aio_pika.RobustChannel": + """Declare a channel.""" + ... + + +class FakeChannelManager(ChannelManager): + def connect(self, connection: "aio_pika.RobustConnection") -> None: + raise NotImplementedError + + def disconnect(self) -> None: + raise NotImplementedError -class ChannelManager: + async def get_channel( + self, + channel: Optional["Channel"] = None, + ) -> "aio_pika.RobustChannel": + raise NotImplementedError + + +class ChannelManagerImpl(ChannelManager): __slots__ = ("__channels", "__connection", "__default_channel") def __init__( self, - connection: "aio_pika.RobustConnection", - *, - default_channel: "Channel", + default_channel: Optional["Channel"] = None, ) -> None: - self.__connection = connection - self.__default_channel = default_channel - self.__channels: Dict[Channel, aio_pika.RobustChannel] = {} + self.__connection: ConnectionState = EmptyConnectionState() + + self.__default_channel = default_channel or Channel() + + self.__channels: dict[Channel, aio_pika.RobustChannel] = {} + + def connect(self, connection: "aio_pika.RobustConnection") -> None: + self.__connection = ConnectedState(connection) + + def disconnect(self) -> None: + self.__connection = EmptyConnectionState() + self.__channels.clear() async def get_channel( self, channel: Optional["Channel"] = None, ) -> "aio_pika.RobustChannel": - """Declare a queue.""" if channel is None: channel = self.__default_channel if (ch := self.__channels.get(channel)) is None: self.__channels[channel] = ch = cast( "aio_pika.RobustChannel", - await self.__connection.channel( + await self.__connection.connection.channel( channel_number=channel.channel_number, publisher_confirms=channel.publisher_confirms, on_return_raises=channel.on_return_raises, diff --git a/faststream/rabbit/helpers/declarer.py b/faststream/rabbit/helpers/declarer.py index f8eca8452e..99529c400d 100644 --- a/faststream/rabbit/helpers/declarer.py +++ b/faststream/rabbit/helpers/declarer.py @@ -1,9 +1,9 @@ -import warnings -from typing import TYPE_CHECKING, Dict, Optional, cast +from typing import TYPE_CHECKING, Optional, Protocol, cast -from typing_extensions import Annotated, deprecated +import aio_pika -from faststream.types import EMPTY +from faststream._internal.constants import EMPTY +from faststream.rabbit.schemas import Channel, RabbitQueue if TYPE_CHECKING: import aio_pika @@ -13,42 +13,84 @@ from .channel_manager import ChannelManager -class RabbitDeclarer: +class RabbitDeclarer(Protocol): """An utility class to declare RabbitMQ queues and exchanges.""" + def disconnect(self) -> None: ... + + async def declare_queue( + self, + queue: "RabbitQueue", + declare: bool = EMPTY, + *, + channel: Optional["Channel"] = None, + ) -> "aio_pika.RobustQueue": + """Declare a queue.""" + ... + + async def declare_exchange( + self, + exchange: "RabbitExchange", + declare: bool = EMPTY, + *, + channel: Optional["Channel"] = None, + ) -> "aio_pika.RobustExchange": + """Declare an exchange, parent exchanges and bind them each other.""" + ... + + +class FakeRabbitDeclarer(RabbitDeclarer): + def disconnect(self) -> None: + raise NotImplementedError + + async def declare_queue( + self, + queue: "RabbitQueue", + declare: bool = EMPTY, + *, + channel: Optional["Channel"] = None, + ) -> "aio_pika.RobustQueue": + raise NotImplementedError + + async def declare_exchange( + self, + exchange: "RabbitExchange", + declare: bool = EMPTY, + *, + channel: Optional["Channel"] = None, + ) -> "aio_pika.RobustExchange": + raise NotImplementedError + + +class RabbitDeclarerImpl(RabbitDeclarer): __slots__ = ("__channel_manager", "__exchanges", "__queues") - def __init__(self, channel_manage: "ChannelManager") -> None: - self.__channel_manager = channel_manage - self.__queues: Dict[RabbitQueue, aio_pika.RobustQueue] = {} - self.__exchanges: Dict[RabbitExchange, aio_pika.RobustExchange] = {} + def __init__(self, channel_manager: "ChannelManager") -> None: + self.__channel_manager = channel_manager + self._queues: dict[RabbitQueue, aio_pika.RobustQueue] = {} + self._exchanges: dict[RabbitExchange, aio_pika.RobustExchange] = {} + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(queues={list(self._queues.keys())}, exchanges={list(self._exchanges.keys())})" + + def disconnect(self) -> None: + self._queues.clear() + self._exchanges.clear() async def declare_queue( self, queue: "RabbitQueue", declare: bool = EMPTY, - passive: Annotated[ - bool, - deprecated("Use `declare` instead. Will be removed in the 0.6.0 release."), - ] = EMPTY, *, channel: Optional["Channel"] = None, ) -> "aio_pika.RobustQueue": - """Declare a queue.""" - if (q := self.__queues.get(queue)) is None: - channel_obj = await self.__channel_manager.get_channel(channel) - if passive is not EMPTY: - warnings.warn( - DeprecationWarning( - "Use `declare` instead. Will be removed in the 0.6.0 release.", - ), - stacklevel=2, - ) - declare = not passive - elif declare is EMPTY: + if (q := self._queues.get(queue)) is None: + if declare is EMPTY: declare = queue.declare - self.__queues[queue] = q = cast( + channel_obj = await self.__channel_manager.get_channel(channel) + + self._queues[queue] = q = cast( "aio_pika.RobustQueue", await channel_obj.declare_queue( name=queue.name, @@ -68,31 +110,19 @@ async def declare_exchange( self, exchange: "RabbitExchange", declare: bool = EMPTY, - passive: Annotated[ - bool, - deprecated("Use `declare` instead. Will be removed in the 0.6.0 release."), - ] = EMPTY, *, channel: Optional["Channel"] = None, ) -> "aio_pika.RobustExchange": - """Declare an exchange, parent exchanges and bind them each other.""" channel_obj = await self.__channel_manager.get_channel(channel) + if not exchange.name: return channel_obj.default_exchange - if (exch := self.__exchanges.get(exchange)) is None: - if passive is not EMPTY: - warnings.warn( - DeprecationWarning( - "Use `declare` instead. Will be removed in the 0.6.0 release.", - ), - stacklevel=2, - ) - declare = not passive - elif declare is EMPTY: + if (exch := self._exchanges.get(exchange)) is None: + if declare is EMPTY: declare = exchange.declare - self.__exchanges[exchange] = exch = cast( + self._exchanges[exchange] = exch = cast( "aio_pika.RobustExchange", await channel_obj.declare_exchange( name=exchange.name, @@ -111,7 +141,7 @@ async def declare_exchange( parent = await self.declare_exchange(exchange.bind_to) await exch.bind( exchange=parent, - routing_key=exchange.routing, + routing_key=exchange.routing(), arguments=exchange.bind_arguments, timeout=exchange.timeout, robust=exchange.robust, diff --git a/faststream/rabbit/helpers/state.py b/faststream/rabbit/helpers/state.py new file mode 100644 index 0000000000..b32e28c048 --- /dev/null +++ b/faststream/rabbit/helpers/state.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING, Protocol + +from faststream.exceptions import IncorrectState + +if TYPE_CHECKING: + from aio_pika import RobustConnection + from typing_extensions import ReadOnly + + +class ConnectionState(Protocol): + connection: "ReadOnly[RobustConnection]" + + +class EmptyConnectionState(ConnectionState): + __slots__ = () + + error_msg = "You should connect broker first." + + @property + def connection(self) -> "RobustConnection": + raise IncorrectState(self.error_msg) + + +class ConnectedState(ConnectionState): + __slots__ = ("connection",) + + def __init__(self, connection: "RobustConnection") -> None: + self.connection = connection diff --git a/faststream/rabbit/message.py b/faststream/rabbit/message.py index 4287cf2fd7..7b91fdd72f 100644 --- a/faststream/rabbit/message.py +++ b/faststream/rabbit/message.py @@ -1,6 +1,6 @@ from aio_pika import IncomingMessage -from faststream.broker.message import StreamMessage +from faststream.message import StreamMessage class RabbitMessage(StreamMessage[IncomingMessage]): diff --git a/faststream/rabbit/opentelemetry/middleware.py b/faststream/rabbit/opentelemetry/middleware.py index 29a553a7f0..60e08d736b 100644 --- a/faststream/rabbit/opentelemetry/middleware.py +++ b/faststream/rabbit/opentelemetry/middleware.py @@ -1,19 +1,19 @@ -from typing import Optional from opentelemetry.metrics import Meter, MeterProvider from opentelemetry.trace import TracerProvider from faststream.opentelemetry.middleware import TelemetryMiddleware from faststream.rabbit.opentelemetry.provider import RabbitTelemetrySettingsProvider +from faststream.rabbit.response import RabbitPublishCommand -class RabbitTelemetryMiddleware(TelemetryMiddleware): +class RabbitTelemetryMiddleware(TelemetryMiddleware[RabbitPublishCommand]): def __init__( self, *, - tracer_provider: Optional[TracerProvider] = None, - meter_provider: Optional[MeterProvider] = None, - meter: Optional[Meter] = None, + tracer_provider: TracerProvider | None = None, + meter_provider: MeterProvider | None = None, + meter: Meter | None = None, ) -> None: super().__init__( settings_provider_factory=lambda _: RabbitTelemetrySettingsProvider(), diff --git a/faststream/rabbit/opentelemetry/provider.py b/faststream/rabbit/opentelemetry/provider.py index 6971810ff2..374285d932 100644 --- a/faststream/rabbit/opentelemetry/provider.py +++ b/faststream/rabbit/opentelemetry/provider.py @@ -1,19 +1,21 @@ -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from opentelemetry.semconv.trace import SpanAttributes from faststream.opentelemetry import TelemetrySettingsProvider from faststream.opentelemetry.consts import MESSAGING_DESTINATION_PUBLISH_NAME +from faststream.rabbit.response import RabbitPublishCommand if TYPE_CHECKING: from aio_pika import IncomingMessage - from faststream.broker.message import StreamMessage - from faststream.rabbit.schemas.exchange import RabbitExchange - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict + from faststream.message import StreamMessage -class RabbitTelemetrySettingsProvider(TelemetrySettingsProvider["IncomingMessage"]): +class RabbitTelemetrySettingsProvider( + TelemetrySettingsProvider["IncomingMessage", RabbitPublishCommand], +): __slots__ = ("messaging_system",) def __init__(self) -> None: @@ -41,26 +43,19 @@ def get_consume_destination_name( routing_key = msg.raw_message.routing_key return f"{exchange}.{routing_key}" - def get_publish_attrs_from_kwargs( + def get_publish_attrs_from_cmd( self, - kwargs: "AnyDict", + cmd: "RabbitPublishCommand", ) -> "AnyDict": - exchange: Union[None, str, RabbitExchange] = kwargs.get("exchange") return { SpanAttributes.MESSAGING_SYSTEM: self.messaging_system, - SpanAttributes.MESSAGING_DESTINATION_NAME: getattr( - exchange, "name", exchange or "" - ), - SpanAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY: kwargs[ - "routing_key" - ], - SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: kwargs["correlation_id"], + SpanAttributes.MESSAGING_DESTINATION_NAME: cmd.exchange.name, + SpanAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY: cmd.destination, + SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: cmd.correlation_id, } def get_publish_destination_name( self, - kwargs: "AnyDict", + cmd: "RabbitPublishCommand", ) -> str: - exchange: str = kwargs.get("exchange") or "default" - routing_key: str = kwargs["routing_key"] - return f"{exchange}.{routing_key}" + return f"{cmd.exchange.name or 'default'}.{cmd.destination}" diff --git a/faststream/rabbit/parser.py b/faststream/rabbit/parser.py index 8fe02dc4b3..951af5c14f 100644 --- a/faststream/rabbit/parser.py +++ b/faststream/rabbit/parser.py @@ -1,9 +1,10 @@ +import datetime from typing import TYPE_CHECKING, Optional from aio_pika import Message from aio_pika.abc import DeliveryMode -from faststream.broker.message import ( +from faststream.message import ( StreamMessage, decode_message, encode_message, @@ -16,9 +17,10 @@ from aio_pika import IncomingMessage from aio_pika.abc import DateType, HeadersType + from fast_depends.library.serializer import SerializerProto + from faststream._internal.basic_types import DecodedMessage from faststream.rabbit.types import AioPikaSendableMessage - from faststream.types import DecodedMessage class AioPikaParser: @@ -61,44 +63,43 @@ async def decode_message( def encode_message( message: "AioPikaSendableMessage", *, - persist: bool, - reply_to: Optional[str], - headers: Optional["HeadersType"], - content_type: Optional[str], - content_encoding: Optional[str], - priority: Optional[int], - correlation_id: Optional[str], - expiration: Optional["DateType"], - message_id: Optional[str], - timestamp: Optional["DateType"], - message_type: Optional[str], - user_id: Optional[str], - app_id: Optional[str], + persist: bool = False, + reply_to: str | None = None, + headers: Optional["HeadersType"] = None, + content_type: str | None = None, + content_encoding: str | None = None, + priority: int | None = None, + correlation_id: str | None = None, + expiration: "DateType" = None, + message_id: str | None = None, + timestamp: "DateType" = None, + message_type: str | None = None, + user_id: str | None = None, + app_id: str | None = None, + serializer: Optional["SerializerProto"] = None, ) -> Message: """Encodes a message for sending using AioPika.""" if isinstance(message, Message): return message + message_body, generated_content_type = encode_message(message, serializer) - else: - message_body, generated_content_type = encode_message(message) - - delivery_mode = ( - DeliveryMode.PERSISTENT if persist else DeliveryMode.NOT_PERSISTENT - ) + delivery_mode = ( + DeliveryMode.PERSISTENT if persist else DeliveryMode.NOT_PERSISTENT + ) - return Message( - message_body, - content_type=content_type or generated_content_type, - delivery_mode=delivery_mode, - reply_to=reply_to, - correlation_id=correlation_id or gen_cor_id(), - headers=headers, - content_encoding=content_encoding, - priority=priority, - expiration=expiration, - message_id=message_id, - timestamp=timestamp, - type=message_type, - user_id=user_id, - app_id=app_id, - ) + return Message( + message_body, + content_type=content_type or generated_content_type, + delivery_mode=delivery_mode, + reply_to=reply_to, + correlation_id=correlation_id or gen_cor_id(), + headers=headers, + content_encoding=content_encoding, + priority=priority, + expiration=expiration, + message_id=message_id, + timestamp=timestamp or datetime.datetime.now(tz=datetime.timezone.utc), + type=message_type, + user_id=user_id, + app_id=app_id, + ) diff --git a/faststream/rabbit/prometheus/middleware.py b/faststream/rabbit/prometheus/middleware.py index b2f96e45ca..7de0d81feb 100644 --- a/faststream/rabbit/prometheus/middleware.py +++ b/faststream/rabbit/prometheus/middleware.py @@ -1,21 +1,27 @@ -from typing import TYPE_CHECKING, Optional, Sequence +from collections.abc import Sequence +from typing import TYPE_CHECKING -from faststream.prometheus.middleware import BasePrometheusMiddleware +from aio_pika import IncomingMessage + +from faststream._internal.constants import EMPTY +from faststream.prometheus.middleware import PrometheusMiddleware from faststream.rabbit.prometheus.provider import RabbitMetricsSettingsProvider -from faststream.types import EMPTY +from faststream.rabbit.response import RabbitPublishCommand if TYPE_CHECKING: from prometheus_client import CollectorRegistry -class RabbitPrometheusMiddleware(BasePrometheusMiddleware): +class RabbitPrometheusMiddleware( + PrometheusMiddleware[RabbitPublishCommand, IncomingMessage] +): def __init__( self, *, registry: "CollectorRegistry", app_name: str = EMPTY, metrics_prefix: str = "faststream", - received_messages_size_buckets: Optional[Sequence[float]] = None, + received_messages_size_buckets: Sequence[float] | None = None, ) -> None: super().__init__( settings_provider_factory=lambda _: RabbitMetricsSettingsProvider(), diff --git a/faststream/rabbit/prometheus/provider.py b/faststream/rabbit/prometheus/provider.py index 48c1bb2541..14427f977d 100644 --- a/faststream/rabbit/prometheus/provider.py +++ b/faststream/rabbit/prometheus/provider.py @@ -1,19 +1,20 @@ -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from faststream.prometheus import ( ConsumeAttrs, MetricsSettingsProvider, ) +from faststream.rabbit.response import RabbitPublishCommand if TYPE_CHECKING: from aio_pika import IncomingMessage - from faststream.broker.message import StreamMessage - from faststream.rabbit.schemas.exchange import RabbitExchange - from faststream.types import AnyDict + from faststream.message.message import StreamMessage -class RabbitMetricsSettingsProvider(MetricsSettingsProvider["IncomingMessage"]): +class RabbitMetricsSettingsProvider( + MetricsSettingsProvider["IncomingMessage", RabbitPublishCommand], +): __slots__ = ("messaging_system",) def __init__(self) -> None: @@ -32,13 +33,8 @@ def get_consume_attrs_from_message( "messages_count": 1, } - def get_publish_destination_name_from_kwargs( + def get_publish_destination_name_from_cmd( self, - kwargs: "AnyDict", + cmd: RabbitPublishCommand, ) -> str: - exchange: Union[None, str, RabbitExchange] = kwargs.get("exchange") - exchange_prefix = getattr(exchange, "name", exchange or "default") - - routing_key: str = kwargs["routing_key"] - - return f"{exchange_prefix}.{routing_key}" + return f"{cmd.exchange.name or 'default'}.{cmd.destination}" diff --git a/faststream/rabbit/publisher/__init__.py b/faststream/rabbit/publisher/__init__.py index e69de29bb2..241ddb8b57 100644 --- a/faststream/rabbit/publisher/__init__.py +++ b/faststream/rabbit/publisher/__init__.py @@ -0,0 +1,5 @@ +from .usecase import RabbitPublisher + +__all__ = ( + "RabbitPublisher", +) diff --git a/faststream/rabbit/publisher/asyncapi.py b/faststream/rabbit/publisher/asyncapi.py deleted file mode 100644 index d8328ab05c..0000000000 --- a/faststream/rabbit/publisher/asyncapi.py +++ /dev/null @@ -1,137 +0,0 @@ -from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence - -from typing_extensions import override - -from faststream.asyncapi.schema import ( - Channel, - ChannelBinding, - CorrelationId, - Message, - Operation, - OperationBinding, -) -from faststream.asyncapi.schema.bindings import amqp -from faststream.asyncapi.utils import resolve_payloads -from faststream.rabbit.publisher.usecase import LogicPublisher, PublishKwargs -from faststream.rabbit.utils import is_routing_exchange - -if TYPE_CHECKING: - from aio_pika import IncomingMessage - - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware - from faststream.rabbit.schemas import RabbitExchange, RabbitQueue - - -class AsyncAPIPublisher(LogicPublisher): - """AsyncAPI-compatible Rabbit Publisher class. - - Creting by - - ```python - publisher: AsyncAPIPublisher = broker.publisher(...) - # or - publisher: AsyncAPIPublisher = router.publisher(...) - ``` - """ - - def get_name(self) -> str: - routing = ( - self.routing_key - or (self.queue.routing if is_routing_exchange(self.exchange) else None) - or "_" - ) - - return f"{routing}:{getattr(self.exchange, 'name', None) or '_'}:Publisher" - - def get_schema(self) -> Dict[str, Channel]: - payloads = self.get_payloads() - - return { - self.name: Channel( - description=self.description, - publish=Operation( - bindings=OperationBinding( - amqp=amqp.OperationBinding( - cc=self.routing or None, - deliveryMode=2 if self.message_kwargs.get("persist") else 1, - mandatory=self.message_kwargs.get("mandatory"), - replyTo=self.message_kwargs.get("reply_to"), - priority=self.message_kwargs.get("priority"), - ), - ) - if is_routing_exchange(self.exchange) - else None, - message=Message( - title=f"{self.name}:Message", - payload=resolve_payloads( - payloads, - "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, - ) - ), - } - ) - ), - ) - } - - @override - @classmethod - def create( # type: ignore[override] - cls, - *, - routing_key: str, - queue: "RabbitQueue", - exchange: "RabbitExchange", - message_kwargs: "PublishKwargs", - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[IncomingMessage]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> "AsyncAPIPublisher": - return cls( - routing_key=routing_key, - queue=queue, - exchange=exchange, - message_kwargs=message_kwargs, - # Publisher args - broker_middlewares=broker_middlewares, - middlewares=middlewares, - # AsyncAPI args - schema_=schema_, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) diff --git a/faststream/rabbit/publisher/config.py b/faststream/rabbit/publisher/config.py new file mode 100644 index 0000000000..d04184b9d9 --- /dev/null +++ b/faststream/rabbit/publisher/config.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from faststream._internal.configs import ( + PublisherSpecificationConfig, + PublisherUsecaseConfig, +) +from faststream.rabbit.configs.base import RabbitConfig, RabbitEndpointConfig + +if TYPE_CHECKING: + from .options import PublishKwargs + + +@dataclass(kw_only=True) +class RabbitPublisherSpecificationConfig( + RabbitConfig, PublisherSpecificationConfig, +): + routing_key: str + message_kwargs: "PublishKwargs" + + +@dataclass(kw_only=True) +class RabbitPublisherConfig(RabbitEndpointConfig, PublisherUsecaseConfig): + routing_key: str + message_kwargs: "PublishKwargs" diff --git a/faststream/rabbit/publisher/factory.py b/faststream/rabbit/publisher/factory.py new file mode 100644 index 0000000000..461800c035 --- /dev/null +++ b/faststream/rabbit/publisher/factory.py @@ -0,0 +1,61 @@ +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +from .config import RabbitPublisherConfig, RabbitPublisherSpecificationConfig +from .specification import RabbitPublisherSpecification +from .usecase import RabbitPublisher + +if TYPE_CHECKING: + from faststream._internal.types import PublisherMiddleware + from faststream.rabbit.configs import RabbitBrokerConfig + from faststream.rabbit.schemas import RabbitExchange, RabbitQueue + + from .usecase import PublishKwargs + + +def create_publisher( + *, + routing_key: str, + queue: "RabbitQueue", + exchange: "RabbitExchange", + message_kwargs: "PublishKwargs", + # Broker args + config: "RabbitBrokerConfig", + # Publisher args + middlewares: Sequence["PublisherMiddleware"], + # Specification args + schema_: Any | None, + title_: str | None, + description_: str | None, + include_in_schema: bool, +) -> RabbitPublisher: + publisher_config = RabbitPublisherConfig( + routing_key=routing_key, + message_kwargs=message_kwargs, + queue=queue, + exchange=exchange, + # publisher + middlewares=middlewares, + # broker + _outer_config=config, + ) + + specification = RabbitPublisherSpecification( + _outer_config=config, + specification_config=RabbitPublisherSpecificationConfig( + message_kwargs=message_kwargs, + routing_key=routing_key, + queue=queue, + exchange=exchange, + # specification options + schema_=schema_, + title_=title_, + description_=description_, + include_in_schema=include_in_schema, + ), + ) + + return RabbitPublisher( + config=publisher_config, + specification=specification, + ) diff --git a/faststream/rabbit/publisher/fake.py b/faststream/rabbit/publisher/fake.py new file mode 100644 index 0000000000..885f44a8fb --- /dev/null +++ b/faststream/rabbit/publisher/fake.py @@ -0,0 +1,32 @@ +from typing import TYPE_CHECKING, Union + +from faststream._internal.endpoint.publisher.fake import FakePublisher +from faststream.rabbit.response import RabbitPublishCommand + +if TYPE_CHECKING: + from faststream._internal.producer import ProducerProto + from faststream.response.response import PublishCommand + + +class RabbitFakePublisher(FakePublisher): + """Publisher Interface implementation to use as RPC or REPLY TO answer publisher.""" + + def __init__( + self, + producer: "ProducerProto", + routing_key: str, + app_id: str | None, + ) -> None: + super().__init__(producer=producer) + self.routing_key = routing_key + self.app_id = app_id + + def patch_command( + self, cmd: Union["PublishCommand", "RabbitPublishCommand"] + ) -> "RabbitPublishCommand": + cmd = super().patch_command(cmd) + real_cmd = RabbitPublishCommand.from_cmd(cmd) + real_cmd.destination = self.routing_key + if self.app_id: + real_cmd.message_options["app_id"] = self.app_id + return real_cmd diff --git a/faststream/rabbit/publisher/options.py b/faststream/rabbit/publisher/options.py new file mode 100644 index 0000000000..003dbf8225 --- /dev/null +++ b/faststream/rabbit/publisher/options.py @@ -0,0 +1,38 @@ +from typing import TYPE_CHECKING, Optional + +from typing_extensions import TypedDict + +if TYPE_CHECKING: + from aio_pika.abc import DateType, HeadersType, TimeoutType + + +class PublishOptions(TypedDict, total=False): + mandatory: bool + immediate: bool + timeout: "TimeoutType" + + +class MessageOptions(TypedDict, total=False): + persist: bool + reply_to: str | None + headers: Optional["HeadersType"] + content_type: str | None + content_encoding: str | None + priority: int | None + expiration: "DateType" + message_id: str | None + timestamp: "DateType" + message_type: str | None + user_id: str | None + app_id: str | None + correlation_id: str | None + + +class RequestPublishKwargs(MessageOptions, PublishOptions, total=False): + """Typed dict to annotate RabbitMQ requesters.""" + + +class PublishKwargs(MessageOptions, PublishOptions, total=False): + """Typed dict to annotate RabbitMQ publishers.""" + + reply_to: str | None diff --git a/faststream/rabbit/publisher/producer.py b/faststream/rabbit/publisher/producer.py index 466b68c168..c595a9ba3e 100644 --- a/faststream/rabbit/publisher/producer.py +++ b/faststream/rabbit/publisher/producer.py @@ -1,42 +1,116 @@ -import datetime from typing import ( TYPE_CHECKING, - Any, - AsyncContextManager, Optional, - Type, - Union, + Protocol, cast, ) import anyio -from typing_extensions import override +from typing_extensions import ReadOnly, Unpack, override -from faststream.broker.publisher.proto import ProducerProto -from faststream.broker.utils import resolve_custom_func -from faststream.exceptions import WRONG_PUBLISH_ARGS +from faststream._internal.endpoint.utils import resolve_custom_func +from faststream._internal.producer import ProducerProto +from faststream.exceptions import FeatureNotSupportedException, IncorrectState from faststream.rabbit.parser import AioPikaParser from faststream.rabbit.schemas import RABBIT_REPLY, RabbitExchange -from faststream.utils.functions import fake_context, timeout_scope if TYPE_CHECKING: from types import TracebackType import aiormq from aio_pika import IncomingMessage, RobustQueue - from aio_pika.abc import AbstractIncomingMessage, DateType, HeadersType, TimeoutType + from aio_pika.abc import AbstractIncomingMessage, TimeoutType from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream + from fast_depends.library.serializer import SerializerProto - from faststream.broker.types import ( + from faststream._internal.types import ( AsyncCallable, CustomCallable, ) from faststream.rabbit.helpers import RabbitDeclarer + from faststream.rabbit.response import MessageOptions, RabbitPublishCommand from faststream.rabbit.types import AioPikaSendableMessage - from faststream.types import SendableMessage + + +class LockState(Protocol): + lock: ReadOnly["anyio.Lock"] + + +class LockUnset(LockState): + __slots__ = () + + @property + def lock(self) -> "anyio.Lock": + msg = "You should call `producer.connect()` method at first." + raise IncorrectState(msg) + + +class RealLock(LockState): + __slots__ = ("lock",) + + def __init__(self) -> None: + self.lock = anyio.Lock() class AioPikaFastProducer(ProducerProto): + def connect(self, serializer: Optional["SerializerProto"] = None) -> None: ... + + def disconnect(self) -> None: ... + + @override + async def publish( # type: ignore[override] + self, + cmd: "RabbitPublishCommand", + ) -> Optional["aiormq.abc.ConfirmationFrameType"]: + """Publish a message to a RabbitMQ queue.""" + + @override + async def request( # type: ignore[override] + self, + cmd: "RabbitPublishCommand", + ) -> "IncomingMessage": + """Publish a message to a RabbitMQ queue.""" + + @override + async def publish_batch( + self, + cmd: "RabbitPublishCommand", + ) -> None: ... + + +class FakeAioPikaFastProducer(AioPikaFastProducer): + def __bool__(self) -> bool: + return False + + def connect(self, serializer: Optional["SerializerProto"] = None) -> None: + raise NotImplementedError + + def disconnect(self) -> None: + raise NotImplementedError + + @override + async def publish( # type: ignore[override] + self, + cmd: "RabbitPublishCommand", + ) -> Optional["aiormq.abc.ConfirmationFrameType"]: + raise NotImplementedError + + @override + async def request( # type: ignore[override] + self, + cmd: "RabbitPublishCommand", + ) -> "IncomingMessage": + raise NotImplementedError + + @override + async def publish_batch( + self, + cmd: "RabbitPublishCommand", + ) -> None: + raise NotImplementedError + + +class AioPikaFastProducerImpl(AioPikaFastProducer): """A class for fast producing messages using aio-pika.""" _decoder: "AsyncCallable" @@ -51,139 +125,59 @@ def __init__( ) -> None: self.declarer = declarer - self._rpc_lock = anyio.Lock() + self.__lock: LockState = LockUnset() + self.serializer: SerializerProto | None = None default_parser = AioPikaParser() self._parser = resolve_custom_func(parser, default_parser.parse_message) self._decoder = resolve_custom_func(decoder, default_parser.decode_message) + def connect(self, serializer: Optional["SerializerProto"] = None) -> None: + """Lock initialization. + + Should be called in async context due `anyio.Lock` object can't be created outside event loop. + """ + self.serializer = serializer + self.__lock = RealLock() + + def disconnect(self) -> None: + self.__lock = LockUnset() + @override async def publish( # type: ignore[override] self, - message: "AioPikaSendableMessage", - exchange: Union["RabbitExchange", str, None] = None, - *, - correlation_id: str = "", - routing_key: str = "", - mandatory: bool = True, - immediate: bool = False, - timeout: "TimeoutType" = None, - rpc: bool = False, - rpc_timeout: Optional[float] = 30.0, - raise_timeout: bool = False, - persist: bool = False, - reply_to: Optional[str] = None, - headers: Optional["HeadersType"] = None, - content_type: Optional[str] = None, - content_encoding: Optional[str] = None, - priority: Optional[int] = None, - expiration: Optional["DateType"] = None, - message_id: Optional[str] = None, - timestamp: Optional["DateType"] = None, - message_type: Optional[str] = None, - user_id: Optional[str] = None, - app_id: Optional[str] = None, - ) -> Optional[Any]: - """Publish a message to a RabbitMQ queue.""" - context: AsyncContextManager[ - Optional[MemoryObjectReceiveStream[IncomingMessage]] - ] - if rpc: - if reply_to is not None: - raise WRONG_PUBLISH_ARGS - - context = _RPCCallback( - self._rpc_lock, - await self.declarer.declare_queue(RABBIT_REPLY), - ) - else: - context = fake_context() - - async with context as response_queue: - r = await self._publish( - message=message, - exchange=exchange, - routing_key=routing_key, - mandatory=mandatory, - immediate=immediate, - timeout=timeout, - persist=persist, - reply_to=reply_to if response_queue is None else RABBIT_REPLY.name, - headers=headers, - content_type=content_type, - content_encoding=content_encoding, - priority=priority, - correlation_id=correlation_id, - expiration=expiration, - message_id=message_id, - timestamp=timestamp, - message_type=message_type, - user_id=user_id, - app_id=app_id, - ) - - if response_queue is None: - return r - - else: - msg: Optional[IncomingMessage] = None - with timeout_scope(rpc_timeout, raise_timeout): - msg = await response_queue.receive() - - if msg: # pragma: no branch - return await self._decoder(await self._parser(msg)) - - return None + cmd: "RabbitPublishCommand", + ) -> Optional["aiormq.abc.ConfirmationFrameType"]: + return await self._publish( + message=cmd.body, + exchange=cmd.exchange, + routing_key=cmd.destination, + reply_to=cmd.reply_to, + headers=cmd.headers, + correlation_id=cmd.correlation_id, + **cmd.publish_options, + **cmd.message_options, + ) @override async def request( # type: ignore[override] self, - message: "AioPikaSendableMessage", - exchange: Union["RabbitExchange", str, None] = None, - *, - correlation_id: str = "", - routing_key: str = "", - mandatory: bool = True, - immediate: bool = False, - timeout: Optional[float] = None, - persist: bool = False, - headers: Optional["HeadersType"] = None, - content_type: Optional[str] = None, - content_encoding: Optional[str] = None, - priority: Optional[int] = None, - expiration: Optional["DateType"] = None, - message_id: Optional[str] = None, - timestamp: Optional["DateType"] = None, - message_type: Optional[str] = None, - user_id: Optional[str] = None, - app_id: Optional[str] = None, + cmd: "RabbitPublishCommand", ) -> "IncomingMessage": - """Publish a message to a RabbitMQ queue.""" async with _RPCCallback( - self._rpc_lock, + self.__lock.lock, await self.declarer.declare_queue(RABBIT_REPLY), ) as response_queue: - with anyio.fail_after(timeout): + with anyio.fail_after(cmd.timeout): await self._publish( - message=message, - exchange=exchange, - routing_key=routing_key, - mandatory=mandatory, - immediate=immediate, - timeout=timeout, - persist=persist, + message=cmd.body, + exchange=cmd.exchange, + routing_key=cmd.destination, reply_to=RABBIT_REPLY.name, - headers=headers, - content_type=content_type, - content_encoding=content_encoding, - priority=priority, - correlation_id=correlation_id, - expiration=expiration, - message_id=message_id, - timestamp=timestamp, - message_type=message_type, - user_id=user_id, - app_id=app_id, + headers=cmd.headers, + correlation_id=cmd.correlation_id, + **cmd.publish_options, + **cmd.message_options, ) return await response_queue.receive() @@ -191,48 +185,17 @@ async def _publish( self, message: "AioPikaSendableMessage", *, - correlation_id: str, - exchange: Union["RabbitExchange", str, None], + exchange: "RabbitExchange", routing_key: str, - mandatory: bool, - immediate: bool, - timeout: "TimeoutType", - persist: bool, - reply_to: Optional[str], - headers: Optional["HeadersType"], - content_type: Optional[str], - content_encoding: Optional[str], - priority: Optional[int], - expiration: Optional["DateType"], - message_id: Optional[str], - timestamp: Optional["DateType"], - message_type: Optional[str], - user_id: Optional[str], - app_id: Optional[str], - ) -> Union["aiormq.abc.ConfirmationFrameType", "SendableMessage"]: - """Publish a message to a RabbitMQ exchange.""" - if timestamp is None: - timestamp = datetime.datetime.now(tz=datetime.timezone.utc) - - message = AioPikaParser.encode_message( - message=message, - persist=persist, - reply_to=reply_to, - headers=headers, - content_type=content_type, - content_encoding=content_encoding, - priority=priority, - correlation_id=correlation_id, - expiration=expiration, - message_id=message_id, - timestamp=timestamp, - message_type=message_type, - user_id=user_id, - app_id=app_id, - ) + mandatory: bool = True, + immediate: bool = False, + timeout: "TimeoutType" = None, + **message_options: Unpack["MessageOptions"], + ) -> Optional["aiormq.abc.ConfirmationFrameType"]: + message = AioPikaParser.encode_message(message=message, serializer=self.serializer, **message_options) exchange_obj = await self.declarer.declare_exchange( - exchange=RabbitExchange.validate(exchange), + exchange=exchange, declare=False, ) @@ -244,6 +207,14 @@ async def _publish( timeout=timeout, ) + @override + async def publish_batch( + self, + cmd: "RabbitPublishCommand", + ) -> None: + msg = "RabbitMQ doesn't support publishing in batches." + raise FeatureNotSupportedException(msg) + class _RPCCallback: """A class provides an RPC lock.""" @@ -274,8 +245,8 @@ async def __aenter__(self) -> "MemoryObjectReceiveStream[IncomingMessage]": async def __aexit__( self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, exc_tb: Optional["TracebackType"] = None, ) -> None: self.lock.release() diff --git a/faststream/rabbit/publisher/specification.py b/faststream/rabbit/publisher/specification.py new file mode 100644 index 0000000000..4ff26d8875 --- /dev/null +++ b/faststream/rabbit/publisher/specification.py @@ -0,0 +1,80 @@ +from faststream._internal.endpoint.publisher import PublisherSpecification +from faststream.rabbit.configs import RabbitBrokerConfig +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 .config import RabbitPublisherSpecificationConfig + + +class RabbitPublisherSpecification(PublisherSpecification[RabbitBrokerConfig, RabbitPublisherSpecificationConfig]): + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + if self.config.routing_key: + routing: str | None = self.config.routing_key + + elif is_routing_exchange(self.config.exchange): + routing = self.config.queue.routing() + + else: + routing = None + + exchange_name = getattr(self.config.exchange, "name", None) + + return f"{routing or '_'}:{exchange_name or '_'}:Publisher" + + def get_schema(self) -> dict[str, "PublisherSpec"]: + payloads = self.get_payloads() + + exchange_binding = amqp.Exchange.from_exchange(self.config.exchange) + queue_binding = amqp.Queue.from_queue(self.config.queue) + + r = self.config.routing_key or self.config.queue.routing() + routing_key = f"{self._outer_config.prefix}{r}" + + return { + self.name: PublisherSpec( + description=self.config.description_, + operation=Operation( + bindings=OperationBinding( + amqp=amqp.OperationBinding( + routing_key=routing_key or None, + queue=queue_binding, + exchange=exchange_binding, + ack=True, + persist=self.config.message_kwargs.get("persist"), + priority=self.config.message_kwargs.get("priority"), + reply_to=self.config.message_kwargs.get("reply_to"), + mandatory=self.config.message_kwargs.get("mandatory"), + ), + ), + message=Message( + title=f"{self.name}:Message", + payload=resolve_payloads( + payloads, + "Publisher", + served_words=2 if self.config.title_ is None else 1, + ), + ), + ), + bindings=ChannelBinding( + amqp=amqp.ChannelBinding( + virtual_host=self._outer_config.virtual_host, + queue=queue_binding, + exchange=exchange_binding, + ), + ), + ), + } diff --git a/faststream/rabbit/publisher/usecase.py b/faststream/rabbit/publisher/usecase.py index 1d58895df6..9b1f0e1dfb 100644 --- a/faststream/rabbit/publisher/usecase.py +++ b/faststream/rabbit/publisher/usecase.py @@ -1,386 +1,155 @@ -from contextlib import AsyncExitStack -from copy import deepcopy -from functools import partial -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Iterable, - Optional, - Sequence, - Union, -) +from collections.abc import Iterable +from typing import TYPE_CHECKING, Optional, Union from aio_pika import IncomingMessage -from typing_extensions import Annotated, Doc, TypedDict, Unpack, deprecated, override +from typing_extensions import Unpack, override -from faststream.broker.message import SourceType, gen_cor_id -from faststream.broker.publisher.usecase import PublisherUsecase -from faststream.exceptions import NOT_CONNECTED_YET -from faststream.rabbit.schemas import BaseRMQInformation, RabbitQueue -from faststream.rabbit.subscriber.usecase import LogicSubscriber -from faststream.utils.functions import return_input +from faststream._internal.endpoint.publisher import PublisherUsecase +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 RabbitExchange, RabbitQueue +from faststream.response.publish_type import PublishType + +from .options import MessageOptions, PublishKwargs, PublishOptions, RequestPublishKwargs if TYPE_CHECKING: - from aio_pika.abc import DateType, HeadersType, TimeoutType + import aiormq - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware + from faststream._internal.types import PublisherMiddleware + from faststream.rabbit.configs import RabbitBrokerConfig from faststream.rabbit.message import RabbitMessage - from faststream.rabbit.publisher.producer import AioPikaFastProducer - from faststream.rabbit.schemas.exchange import RabbitExchange from faststream.rabbit.types import AioPikaSendableMessage - from faststream.types import AnyDict, AsyncFunc - - -# should be public to use in imports -class RequestPublishKwargs(TypedDict, total=False): - """Typed dict to annotate RabbitMQ requesters.""" + from faststream.response.response import PublishCommand - headers: Annotated[ - Optional["HeadersType"], - Doc( - "Message headers to store metainformation. " - "Can be overridden by `publish.headers` if specified." - ), - ] - mandatory: Annotated[ - Optional[bool], - Doc( - "Client waits for confirmation that the message is placed to some queue. " - "RabbitMQ returns message to client if there is no suitable queue." - ), - ] - immediate: Annotated[ - Optional[bool], - Doc( - "Client expects that there is consumer ready to take the message to work. " - "RabbitMQ returns message to client if there is no suitable consumer." - ), - ] - timeout: Annotated[ - "TimeoutType", - Doc("Send confirmation time from RabbitMQ."), - ] - persist: Annotated[ - Optional[bool], - Doc("Restore the message on RabbitMQ reboot."), - ] + from .config import RabbitPublisherConfig + from .specification import RabbitPublisherSpecification - priority: Annotated[ - Optional[int], - Doc("The message priority (0 by default)."), - ] - message_type: Annotated[ - Optional[str], - Doc("Application-specific message type, e.g. **orders.created**."), - ] - content_type: Annotated[ - Optional[str], - Doc( - "Message **content-type** header. " - "Used by application, not core RabbitMQ. " - "Will be set automatically if not specified." - ), - ] - user_id: Annotated[ - Optional[str], - Doc("Publisher connection User ID, validated if set."), - ] - expiration: Annotated[ - Optional["DateType"], - Doc("Message expiration (lifetime) in seconds (or datetime or timedelta)."), - ] - content_encoding: Annotated[ - Optional[str], - Doc("Message body content encoding, e.g. **gzip**."), - ] +class RabbitPublisher(PublisherUsecase[IncomingMessage]): + """A class to represent a RabbitMQ publisher.""" -class PublishKwargs(RequestPublishKwargs, total=False): - """Typed dict to annotate RabbitMQ publishers.""" + _outer_config: "RabbitBrokerConfig" - reply_to: Annotated[ - Optional[str], - Doc( - "Reply message routing key to send with (always sending to default exchange)." - ), - ] + def __init__(self, config: "RabbitPublisherConfig", specification: "RabbitPublisherSpecification") -> None: + super().__init__(config, specification) + self.queue = config.queue + self.routing_key = config.routing_key -class LogicPublisher( - PublisherUsecase[IncomingMessage], - BaseRMQInformation, -): - """A class to represent a RabbitMQ publisher.""" + self.exchange = config.exchange - app_id: Optional[str] + self.headers = config.message_kwargs.pop("headers") or {} + self.reply_to = config.message_kwargs.pop("reply_to", None) or "" + self.timeout = config.message_kwargs.pop("timeout", None) - _producer: Optional["AioPikaFastProducer"] - - def __init__( - self, - *, - routing_key: str, - queue: "RabbitQueue", - exchange: "RabbitExchange", - message_kwargs: "PublishKwargs", - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[IncomingMessage]"], - middlewares: Sequence["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, - ) + message_options, _ = filter_by_dict(MessageOptions, dict(config.message_kwargs)) + self._message_options = message_options - self.routing_key = routing_key + publish_options, _ = filter_by_dict(PublishOptions, dict(config.message_kwargs)) + self.publish_options = publish_options - request_kwargs = dict(message_kwargs) - self.reply_to = request_kwargs.pop("reply_to", None) - self.message_kwargs = request_kwargs - - # BaseRMQInformation - self.queue = queue - self.exchange = exchange + @property + def message_options(self) -> "MessageOptions": + if self._outer_config.app_id and "app_id" not in self._message_options: + message_options = self._message_options.copy() + message_options["app_id"] = self._outer_config.app_id + return message_options - # Setup it later - self.app_id = None - self.virtual_host = "" + return self._message_options - @override - def setup( # type: ignore[override] + def routing( self, *, - producer: Optional["AioPikaFastProducer"], - app_id: Optional[str], - virtual_host: str, - ) -> None: - self.app_id = app_id - self.virtual_host = virtual_host - super().setup(producer=producer) - - @property - def routing(self) -> str: - """Return real routing_key of Publisher.""" - return self.routing_key or self.queue.routing - - def __hash__(self) -> int: - return LogicSubscriber.get_routing_hash(self.queue, self.exchange) + hash( - self.routing_key - ) + queue: Union["RabbitQueue", str, None] = None, + routing_key: str = "", + ) -> str: + if not routing_key: + if q := RabbitQueue.validate(queue): + routing_key = q.routing() + else: + r = self.routing_key or self.queue.routing() + routing_key = f"{self._outer_config.prefix}{r}" + + return routing_key + + async def start(self) -> None: + if self.exchange is not None: + await self._outer_config.declarer.declare_exchange(self.exchange) + return await super().start() @override async def publish( self, message: "AioPikaSendableMessage", - queue: Annotated[ - Union["RabbitQueue", str, None], - Doc("Message routing key to publish with."), - ] = None, - exchange: Annotated[ - Union["RabbitExchange", str, None], - Doc("Target exchange to publish message to."), - ] = None, + queue: Union["RabbitQueue", str, None] = None, + exchange: Union["RabbitExchange", str, None] = None, *, - routing_key: Annotated[ - str, - Doc( - "Message routing key to publish with. " - "Overrides `queue` option if presented." - ), - ] = "", + routing_key: str = "", # message args - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - message_id: Annotated[ - Optional[str], - Doc("Arbitrary message id. Generated automatically if not present."), - ] = None, - timestamp: Annotated[ - Optional["DateType"], - Doc("Message publish timestamp. Generated automatically if not present."), - ] = None, - # rpc args - rpc: Annotated[ - bool, - Doc("Whether to wait for reply in blocking mode."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - rpc_timeout: Annotated[ - Optional[float], - Doc("RPC reply waiting time."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method with `timeout` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = 30.0, - raise_timeout: Annotated[ - bool, - Doc( - "Whetever to raise `TimeoutError` or return `None` at **rpc_timeout**. " - "RPC request returns `None` at timeout by default." - ), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "`request` always raises TimeoutError instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, + correlation_id: str | None = None, # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), **publish_kwargs: "Unpack[PublishKwargs]", - ) -> Optional[Any]: - assert self._producer, NOT_CONNECTED_YET # nosec B101 + ) -> Optional["aiormq.abc.ConfirmationFrameType"]: + headers = self.headers | publish_kwargs.pop("headers", {}) + cmd = RabbitPublishCommand( + message, + routing_key=self.routing(queue=queue, routing_key=routing_key), + exchange=RabbitExchange.validate(exchange or self.exchange), + correlation_id=correlation_id or gen_cor_id(), + headers=headers, + _publish_type=PublishType.PUBLISH, + **(self.publish_options | self.message_options | publish_kwargs), + ) + + frame: aiormq.abc.ConfirmationFrameType | None = await self._basic_publish( + cmd, + _extra_middlewares=(), + ) + return frame + + @override + async def _publish( + self, + cmd: Union["RabbitPublishCommand", "PublishCommand"], + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> Optional["aiormq.abc.ConfirmationFrameType"]: + """This method should be called in subscriber flow only.""" + cmd = RabbitPublishCommand.from_cmd(cmd) - kwargs: AnyDict = { - "routing_key": routing_key - or self.routing_key - or RabbitQueue.validate(queue or self.queue).routing, - "exchange": exchange or self.exchange.name, - "app_id": self.app_id, - "correlation_id": correlation_id or gen_cor_id(), - "message_id": message_id, - "timestamp": timestamp, - # specific args - "rpc": rpc, - "rpc_timeout": rpc_timeout, - "raise_timeout": raise_timeout, - "reply_to": self.reply_to, - **self.message_kwargs, - **publish_kwargs, - } + cmd.destination = self.routing() + cmd.reply_to = cmd.reply_to or self.reply_to + cmd.add_headers(self.headers, override=False) - call: AsyncFunc = self._producer.publish + cmd.timeout = cmd.timeout or self.timeout - for m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - call = partial(m, call) + cmd.message_options = {**self.message_options, **cmd.message_options} + cmd.publish_options = {**self.publish_options, **cmd.publish_options} - return await call(message, **kwargs) + return await self._basic_publish(cmd, _extra_middlewares=_extra_middlewares) @override async def request( self, message: "AioPikaSendableMessage", - queue: Annotated[ - Union["RabbitQueue", str, None], - Doc("Message routing key to publish with."), - ] = None, - exchange: Annotated[ - Union["RabbitExchange", str, None], - Doc("Target exchange to publish message to."), - ] = None, + queue: Union["RabbitQueue", str, None] = None, + exchange: Union["RabbitExchange", str, None] = None, *, - routing_key: Annotated[ - str, - Doc( - "Message routing key to publish with. " - "Overrides `queue` option if presented." - ), - ] = "", - # message args - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - message_id: Annotated[ - Optional[str], - Doc("Arbitrary message id. Generated automatically if not present."), - ] = None, - timestamp: Annotated[ - Optional["DateType"], - Doc("Message publish timestamp. Generated automatically if not present."), - ] = None, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), + routing_key: str = "", + correlation_id: str | None = None, **publish_kwargs: "Unpack[RequestPublishKwargs]", ) -> "RabbitMessage": - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - kwargs: AnyDict = { - "routing_key": routing_key - or self.routing_key - or RabbitQueue.validate(queue or self.queue).routing, - "exchange": exchange or self.exchange.name, - "app_id": self.app_id, - "correlation_id": correlation_id or gen_cor_id(), - "message_id": message_id, - "timestamp": timestamp, - # specific args - **self.message_kwargs, - **publish_kwargs, - } - - request: AsyncFunc = self._producer.request - - for pub_m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - request = partial(pub_m, request) - - published_msg = await request( + headers = self.headers | publish_kwargs.pop("headers", {}) + cmd = RabbitPublishCommand( message, - **kwargs, + routing_key=self.routing(queue=queue, routing_key=routing_key), + exchange=RabbitExchange.validate(exchange or self.exchange), + correlation_id=correlation_id or gen_cor_id(), + headers=headers, + _publish_type=PublishType.PUBLISH, + **(self.publish_options | self.message_options | publish_kwargs), ) - async with AsyncExitStack() as stack: - return_msg: Callable[[RabbitMessage], Awaitable[RabbitMessage]] = ( - return_input - ) - for m in self._broker_middlewares[::-1]: - mid = m(published_msg) - await stack.enter_async_context(mid) - return_msg = partial(mid.consume_scope, return_msg) - - parsed_msg = await self._producer._parser(published_msg) - parsed_msg._decoded_body = await self._producer._decoder(parsed_msg) - parsed_msg._source_type = SourceType.Response - return await return_msg(parsed_msg) - - raise AssertionError("unreachable") - - def add_prefix(self, prefix: str) -> None: - """Include Publisher in router.""" - new_q = deepcopy(self.queue) - new_q.name = prefix + new_q.name - self.queue = new_q + msg: RabbitMessage = await self._basic_request(cmd) + return msg diff --git a/faststream/rabbit/response.py b/faststream/rabbit/response.py index c145f295dd..5bc9e1b0ce 100644 --- a/faststream/rabbit/response.py +++ b/faststream/rabbit/response.py @@ -1,14 +1,23 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Union -from typing_extensions import override +from typing_extensions import Unpack, override -from faststream.broker.response import Response +from faststream.rabbit.schemas.exchange import RabbitExchange +from faststream.response import PublishCommand, Response +from faststream.response.publish_type import PublishType if TYPE_CHECKING: - from aio_pika.abc import DateType, TimeoutType + from typing import TypedDict + from aio_pika.abc import TimeoutType + + from faststream.rabbit.publisher.options import MessageOptions from faststream.rabbit.types import AioPikaSendableMessage - from faststream.types import AnyDict + + class _PublishOptions(TypedDict): + timeout: TimeoutType + mandatory: bool + immediate: bool class RabbitResponse(Response): @@ -16,49 +25,91 @@ def __init__( self, body: "AioPikaSendableMessage", *, - headers: Optional["AnyDict"] = None, - correlation_id: Optional[str] = None, - message_id: Optional[str] = None, + timeout: "TimeoutType" = None, mandatory: bool = True, immediate: bool = False, - timeout: "TimeoutType" = None, - persist: Optional[bool] = None, - priority: Optional[int] = None, - message_type: Optional[str] = None, - content_type: Optional[str] = None, - expiration: Optional["DateType"] = None, - content_encoding: Optional[str] = None, + **message_options: Unpack["MessageOptions"], ) -> None: + headers = message_options.pop("headers", {}) + correlation_id = message_options.pop("correlation_id", None) + super().__init__( body=body, headers=headers, correlation_id=correlation_id, ) - self.message_id = message_id - self.mandatory = mandatory - self.immediate = immediate - self.timeout = timeout - self.persist = persist - self.priority = priority - self.message_type = message_type - self.content_type = content_type - self.expiration = expiration - self.content_encoding = content_encoding + self.message_options = message_options + self.publish_options: _PublishOptions = { + "mandatory": mandatory, + "immediate": immediate, + "timeout": timeout, + } @override - def as_publish_kwargs(self) -> "AnyDict": - publish_options = { - **super().as_publish_kwargs(), - "message_id": self.message_id, - "mandatory": self.mandatory, - "immediate": self.immediate, - "timeout": self.timeout, - "persist": self.persist, - "priority": self.priority, - "message_type": self.message_type, - "content_type": self.content_type, - "expiration": self.expiration, - "content_encoding": self.content_encoding, + def as_publish_command(self) -> "RabbitPublishCommand": + return RabbitPublishCommand( # type: ignore[misc] + message=self.body, + headers=self.headers, + correlation_id=self.correlation_id, + _publish_type=PublishType.PUBLISH, + # RMQ specific + routing_key="", + **self.publish_options, + **self.message_options, + ) + + +class RabbitPublishCommand(PublishCommand): + def __init__( + self, + message: "AioPikaSendableMessage", + *, + _publish_type: PublishType, + routing_key: str = "", + exchange: RabbitExchange | None = None, + # publish kwargs + mandatory: bool = True, + immediate: bool = False, + timeout: "TimeoutType" = None, + **message_options: Unpack["MessageOptions"], + ) -> None: + headers = message_options.pop("headers", {}) + reply_to = message_options.pop("reply_to", None) or "" + correlation_id = message_options.pop("correlation_id", None) + + super().__init__( + body=message, + destination=routing_key, + correlation_id=correlation_id, + headers=headers, + reply_to=reply_to, + _publish_type=_publish_type, + ) + self.exchange = exchange or RabbitExchange() + + self.timeout = timeout + + self.message_options = message_options + self.publish_options = { + "mandatory": mandatory, + "immediate": immediate, } - return publish_options + + @classmethod + def from_cmd( + cls, + cmd: Union["PublishCommand", "RabbitPublishCommand"], + ) -> "RabbitPublishCommand": + if isinstance(cmd, RabbitPublishCommand): + # NOTE: Should return a copy probably. + return cmd + + return cls( + message=cmd.body, + routing_key=cmd.destination, + correlation_id=cmd.correlation_id, + headers=cmd.headers, + reply_to=cmd.reply_to, + _publish_type=cmd.publish_type, + ) diff --git a/faststream/rabbit/router.py b/faststream/rabbit/router.py deleted file mode 100644 index 7998f41c56..0000000000 --- a/faststream/rabbit/router.py +++ /dev/null @@ -1,360 +0,0 @@ -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Iterable, - Optional, - Sequence, - Union, -) - -from typing_extensions import Annotated, Doc, deprecated - -from faststream.broker.router import ArgsContainer, BrokerRouter, SubscriberRoute -from faststream.broker.utils import default_filter -from faststream.rabbit.broker.registrator import RabbitRegistrator - -if TYPE_CHECKING: - from aio_pika.abc import DateType, HeadersType, TimeoutType - from aio_pika.message import IncomingMessage - from broker.types import PublisherMiddleware - from fast_depends.dependencies import Depends - - from faststream.broker.types import ( - BrokerMiddleware, - CustomCallable, - Filter, - SubscriberMiddleware, - ) - from faststream.rabbit.message import RabbitMessage - from faststream.rabbit.schemas import ( - RabbitExchange, - RabbitQueue, - ) - from faststream.rabbit.schemas.reply import ReplyConfig - from faststream.rabbit.types import AioPikaSendableMessage - from faststream.types import AnyDict - - -class RabbitPublisher(ArgsContainer): - """Delayed RabbitPublisher registration object. - - Just a copy of `RabbitRegistrator.publisher(...)` arguments. - """ - - def __init__( - self, - queue: Annotated[ - Union["RabbitQueue", str], - Doc("Default message routing key to publish with."), - ] = "", - exchange: Annotated[ - Union["RabbitExchange", str, None], - Doc("Target exchange to publish message to."), - ] = None, - *, - routing_key: Annotated[ - str, - Doc( - "Default message routing key to publish with. " - "Overrides `queue` option if presented." - ), - ] = "", - mandatory: Annotated[ - bool, - Doc( - "Client waits for confirmation that the message is placed to some queue. " - "RabbitMQ returns message to client if there is no suitable queue." - ), - ] = True, - immediate: Annotated[ - bool, - Doc( - "Client expects that there is consumer ready to take the message to work. " - "RabbitMQ returns message to client if there is no suitable consumer." - ), - ] = False, - timeout: Annotated[ - "TimeoutType", - Doc("Send confirmation time from RabbitMQ."), - ] = None, - persist: Annotated[ - bool, - Doc("Restore the message on RabbitMQ reboot."), - ] = False, - reply_to: Annotated[ - Optional[str], - Doc( - "Reply message routing key to send with (always sending to default exchange)." - ), - ] = None, - priority: Annotated[ - Optional[int], - Doc("The message priority (0 by default)."), - ] = None, - # basic args - middlewares: Annotated[ - Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI args - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - # message args - headers: Annotated[ - Optional["HeadersType"], - Doc( - "Message headers to store metainformation. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - content_type: Annotated[ - Optional[str], - Doc( - "Message **content-type** header. " - "Used by application, not core RabbitMQ. " - "Will be set automatically if not specified." - ), - ] = None, - content_encoding: Annotated[ - Optional[str], - Doc("Message body content encoding, e.g. **gzip**."), - ] = None, - expiration: Annotated[ - Optional["DateType"], - Doc("Message expiration (lifetime) in seconds (or datetime or timedelta)."), - ] = None, - message_type: Annotated[ - Optional[str], - Doc("Application-specific message type, e.g. **orders.created**."), - ] = None, - user_id: Annotated[ - Optional[str], - Doc("Publisher connection User ID, validated if set."), - ] = None, - ) -> None: - super().__init__( - queue=queue, - exchange=exchange, - routing_key=routing_key, - mandatory=mandatory, - immediate=immediate, - timeout=timeout, - persist=persist, - reply_to=reply_to, - priority=priority, - headers=headers, - content_type=content_type, - content_encoding=content_encoding, - expiration=expiration, - message_type=message_type, - user_id=user_id, - # basic args - middlewares=middlewares, - # AsyncAPI args - title=title, - description=description, - schema=schema, - include_in_schema=include_in_schema, - ) - - -class RabbitRoute(SubscriberRoute): - """Class to store delayed RabbitBroker subscriber registration. - - Just a copy of `RabbitRegistrator.subscriber(...)` arguments. - """ - - def __init__( - self, - call: Annotated[ - Union[ - Callable[..., "AioPikaSendableMessage"], - Callable[..., Awaitable["AioPikaSendableMessage"]], - ], - Doc( - "Message handler function " - "to wrap the same with `@broker.subscriber(...)` way." - ), - ], - queue: Annotated[ - Union[str, "RabbitQueue"], - Doc( - "RabbitMQ queue to listen. " - "**FastStream** declares and binds queue object to `exchange` automatically if it is not passive (by default)." - ), - ], - exchange: Annotated[ - Union[str, "RabbitExchange", None], - Doc( - "RabbitMQ exchange to bind queue to. " - "Uses default exchange if not present. " - "**FastStream** declares exchange object automatically if it is not passive (by default)." - ), - ] = None, - *, - publishers: Annotated[ - Iterable[RabbitPublisher], - Doc("RabbitMQ publishers to broadcast the handler result."), - ] = (), - consume_args: Annotated[ - Optional["AnyDict"], - Doc("Extra consumer arguments to use in `queue.consume(...)` method."), - ] = None, - reply_config: Annotated[ - Optional["ReplyConfig"], - Doc("Extra options to use at replies publishing."), - deprecated( - "Deprecated in **FastStream 0.5.16**. " - "Please, use `RabbitResponse` object as a handler return instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = None, - # broker arguments - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **IncomingMessage** Msg to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - middlewares: Annotated[ - Sequence["SubscriberMiddleware[RabbitMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[RabbitMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), - deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - Union[bool, int], - Doc("Whether to `nack` message at processing exception."), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI information - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - ) -> None: - super().__init__( - call, - publishers=publishers, - queue=queue, - exchange=exchange, - consume_args=consume_args, - reply_config=reply_config, - dependencies=dependencies, - parser=parser, - decoder=decoder, - middlewares=middlewares, - filter=filter, - retry=retry, - no_ack=no_ack, - no_reply=no_reply, - title=title, - description=description, - include_in_schema=include_in_schema, - ) - - -class RabbitRouter( - RabbitRegistrator, - BrokerRouter["IncomingMessage"], -): - """Includable to RabbitBroker router.""" - - def __init__( - self, - prefix: Annotated[ - str, - Doc("String prefix to add to all subscribers queues."), - ] = "", - handlers: Annotated[ - Iterable[RabbitRoute], - Doc("Route object to include."), - ] = (), - *, - dependencies: Annotated[ - Iterable["Depends"], - Doc( - "Dependencies list (`[Depends(),]`) to apply to all routers' publishers/subscribers." - ), - ] = (), - middlewares: Annotated[ - Sequence["BrokerMiddleware[IncomingMessage]"], - Doc("Router middlewares to apply to all routers' publishers/subscribers."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **IncomingMessage** Msg to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - include_in_schema: Annotated[ - Optional[bool], - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = None, - ) -> None: - super().__init__( - handlers=handlers, - # basic args - prefix=prefix, - dependencies=dependencies, - middlewares=middlewares, - parser=parser, - decoder=decoder, - include_in_schema=include_in_schema, - ) diff --git a/faststream/rabbit/schemas/__init__.py b/faststream/rabbit/schemas/__init__.py index b8a3bdcce6..881348e76c 100644 --- a/faststream/rabbit/schemas/__init__.py +++ b/faststream/rabbit/schemas/__init__.py @@ -3,7 +3,6 @@ from .exchange import RabbitExchange from .proto import BaseRMQInformation from .queue import QueueType, RabbitQueue -from .reply import ReplyConfig __all__ = ( "RABBIT_REPLY", @@ -13,7 +12,6 @@ "QueueType", "RabbitExchange", "RabbitQueue", - "ReplyConfig", ) RABBIT_REPLY = RabbitQueue("amq.rabbitmq.reply-to", declare=False) diff --git a/faststream/rabbit/schemas/channel.py b/faststream/rabbit/schemas/channel.py index e716bed616..d5c17a5b9e 100644 --- a/faststream/rabbit/schemas/channel.py +++ b/faststream/rabbit/schemas/channel.py @@ -1,12 +1,11 @@ from dataclasses import dataclass -from typing import Optional @dataclass class Channel: """Channel class that represents a RabbitMQ channel.""" - prefetch_count: Optional[int] = None + prefetch_count: int | None = None """Limit the number of unacknowledged messages on a channel https://www.rabbitmq.com/docs/consumer-prefetch """ @@ -16,7 +15,7 @@ class Channel: https://www.rabbitmq.com/docs/consumer-prefetch#sharing-the-limit """ - channel_number: Optional[int] = None + channel_number: int | None = None """Specify the channel number explicit.""" publisher_confirms: bool = True diff --git a/faststream/rabbit/schemas/exchange.py b/faststream/rabbit/schemas/exchange.py index 804a8a441b..fa915a9d47 100644 --- a/faststream/rabbit/schemas/exchange.py +++ b/faststream/rabbit/schemas/exchange.py @@ -1,11 +1,11 @@ import warnings -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Annotated, Any, Optional, Union -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import Doc, override -from faststream.broker.schemas import NameRequired +from faststream._internal.basic_types import AnyDict +from faststream._internal.proto import NameRequired from faststream.rabbit.schemas.constants import ExchangeType -from faststream.types import EMPTY, AnyDict if TYPE_CHECKING: from aio_pika.abc import TimeoutType @@ -19,17 +19,24 @@ class RabbitExchange(NameRequired): "auto_delete", "bind_arguments", "bind_to", - "declare", "durable", "name", - "passive", "robust", "routing_key", "timeout", "type", ) + def __repr__(self) -> str: + if self.declare: + body = f", robust={self.robust}, durable={self.durable}, auto_delete={self.auto_delete})" + else: + body = "" + + return f"{self.__class__.__name__}({self.name}, type={self.type}, routing_key='{self.routing()}'{body})" + def __hash__(self) -> int: + """Supports hash to store real objects in declarer.""" return sum( ( hash(self.name), @@ -37,10 +44,9 @@ def __hash__(self) -> int: hash(self.routing_key), int(self.durable), int(self.auto_delete), - ) + ), ) - @property def routing(self) -> str: """Return real routing_key of object.""" return self.routing_key or self.name @@ -59,7 +65,7 @@ def __init__( "https://www.rabbitmq.com/tutorials/amqp-concepts#exchanges" "\n" "Or in the FastStream one: " - "https://faststream.airt.ai/latest/rabbit/examples/" + "https://faststream.airt.ai/latest/rabbit/examples/", ), ] = ExchangeType.DIRECT, durable: Annotated[ @@ -70,6 +76,7 @@ def __init__( bool, Doc("The exchange will be deleted after connection closed."), ] = False, + # custom declare: Annotated[ bool, Doc( @@ -78,17 +85,12 @@ def __init__( "Copy of `passive` aio-pike option." ), ] = True, - passive: Annotated[ - bool, - deprecated("Use `declare` instead. Will be removed in the 0.6.0 release."), - Doc("Do not create exchange automatically."), - ] = EMPTY, arguments: Annotated[ - Optional[AnyDict], + AnyDict | None, Doc( "Exchange declarationg arguments. " "You can find usage example in the official RabbitMQ documentation: " - "https://www.rabbitmq.com/docs/ae" + "https://www.rabbitmq.com/docs/ae", ), ] = None, timeout: Annotated[ @@ -104,11 +106,11 @@ def __init__( Doc( "Another `RabbitExchange` object to bind the current one to. " "You can find more information in the official RabbitMQ blog post: " - "https://www.rabbitmq.com/blog/2010/10/19/exchange-to-exchange-bindings" + "https://www.rabbitmq.com/blog/2010/10/19/exchange-to-exchange-bindings", ), ] = None, bind_arguments: Annotated[ - Optional[AnyDict], + AnyDict | None, Doc("Exchange-exchange binding options."), ] = None, routing_key: Annotated[ @@ -133,21 +135,9 @@ def __init__( self.durable = durable self.auto_delete = auto_delete self.robust = robust - self.passive = passive self.timeout = timeout self.arguments = arguments - - if passive is not EMPTY: - warnings.warn( - DeprecationWarning( - "Use `declare` instead. Will be removed in the 0.6.0 release.", - ), - stacklevel=2, - ) - self.declare = not passive - else: - self.declare = declare - + self.declare = declare self.bind_to = bind_to self.bind_arguments = bind_arguments self.routing_key = routing_key diff --git a/faststream/rabbit/schemas/proto.py b/faststream/rabbit/schemas/proto.py index 226840925e..6ab4badc64 100644 --- a/faststream/rabbit/schemas/proto.py +++ b/faststream/rabbit/schemas/proto.py @@ -1,13 +1,30 @@ -from typing import Optional, Protocol +from typing import TYPE_CHECKING, Any -from faststream.rabbit.schemas.exchange import RabbitExchange -from faststream.rabbit.schemas.queue import RabbitQueue +if TYPE_CHECKING: + from faststream.rabbit.configs.specification import RabbitSpecificationConfig + from .exchange import RabbitExchange + from .queue import RabbitQueue -class BaseRMQInformation(Protocol): - """Base class to store AsyncAPI RMQ bindings.""" - virtual_host: str - queue: RabbitQueue - exchange: Optional[RabbitExchange] - app_id: Optional[str] +class BaseRMQInformation: + """Base class to store Specification RMQ bindings.""" + + queue: "RabbitQueue" + exchange: "RabbitExchange" + + def __init__(self, config: "RabbitSpecificationConfig", *args: Any, **kwargs: Any) -> None: + super().__init__(config, *args, **kwargs) + + self.queue = config.queue + self.exchange = config.exchange + + self._outer_config = config.config + + @property + def virtual_host(self) -> str: + return self._outer_config.virtual_host + + @property + def app_id(self) -> str | None: + return self._outer_config.app_id diff --git a/faststream/rabbit/schemas/queue.py b/faststream/rabbit/schemas/queue.py index 4a207e24a1..ccc846c98e 100644 --- a/faststream/rabbit/schemas/queue.py +++ b/faststream/rabbit/schemas/queue.py @@ -1,19 +1,23 @@ -import warnings from copy import deepcopy from enum import Enum -from typing import TYPE_CHECKING, Literal, Optional, TypedDict, Union, overload - -from typing_extensions import Annotated, Doc, deprecated +from typing import ( + TYPE_CHECKING, + Literal, + Optional, + TypedDict, + Union, + overload, +) -from faststream.broker.schemas import NameRequired +from faststream._internal.constants import EMPTY +from faststream._internal.proto import NameRequired +from faststream._internal.utils.path import compile_path from faststream.exceptions import SetupError -from faststream.types import EMPTY -from faststream.utils.path import compile_path if TYPE_CHECKING: from aio_pika.abc import TimeoutType - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict class QueueType(str, Enum): @@ -39,32 +43,51 @@ class RabbitQueue(NameRequired): "arguments", "auto_delete", "bind_arguments", - "declare", "durable", "exclusive", "name", - "passive", "path_regex", "robust", "routing_key", "timeout", ) + def __repr__(self) -> str: + if self.declare: + body = f", robust={self.robust}, durable={self.durable}, exclusive={self.exclusive}, auto_delete={self.auto_delete})" + else: + body = "" + + if (r := self.routing()) != self.name: + body = f", routing_key='{r}'{body}" + + return f"{self.__class__.__name__}({self.name}{body})" + def __hash__(self) -> int: + """Supports hash to store real objects in declarer.""" return sum( ( hash(self.name), int(self.durable), int(self.exclusive), int(self.auto_delete), - ) + ), ) - @property def routing(self) -> str: """Return real routing_key of object.""" return self.routing_key or self.name + def add_prefix(self, prefix: str) -> "RabbitQueue": + new_q: RabbitQueue = deepcopy(self) + + new_q.name = f"{prefix}{new_q.name}" + + if new_q.routing_key: + new_q.routing_key = f"{prefix}{new_q.routing_key}" + + return new_q + @overload def __init__( self, @@ -73,11 +96,6 @@ def __init__( durable: bool = EMPTY, exclusive: bool = False, declare: bool = True, - passive: Annotated[ - bool, - deprecated("Use `declare` instead. Will be removed in the 0.6.0 release."), - Doc("Do not create queue automatically."), - ] = EMPTY, auto_delete: bool = False, arguments: Optional["ClassicQueueArgs"] = None, timeout: "TimeoutType" = None, @@ -94,11 +112,6 @@ def __init__( durable: Literal[True], exclusive: bool = False, declare: bool = True, - passive: Annotated[ - bool, - deprecated("Use `declare` instead. Will be removed in the 0.6.0 release."), - Doc("Do not create queue automatically."), - ] = EMPTY, auto_delete: bool = False, arguments: Optional["QuorumQueueArgs"] = None, timeout: "TimeoutType" = None, @@ -115,11 +128,6 @@ def __init__( durable: Literal[True], exclusive: bool = False, declare: bool = True, - passive: Annotated[ - bool, - deprecated("Use `declare` instead. Will be removed in the 0.6.0 release."), - Doc("Do not create queue automatically."), - ] = EMPTY, auto_delete: bool = False, arguments: Optional["StreamQueueArgs"] = None, timeout: "TimeoutType" = None, @@ -135,11 +143,6 @@ def __init__( durable: bool = EMPTY, exclusive: bool = False, declare: bool = True, - passive: Annotated[ - bool, - deprecated("Use `declare` instead. Will be removed in the 0.6.0 release."), - Doc("Do not create queue automatically."), - ] = EMPTY, auto_delete: bool = False, arguments: Union[ "QuorumQueueArgs", @@ -161,7 +164,6 @@ def __init__( :param declare: Whether to queue automatically or just connect to it. If you want to connect to an existing queue, set this to `False`. Copy of `passive` aio-pike option. - :param passive: Do not create queue automatically. :param auto_delete: The queue will be deleted after connection closed. :param arguments: Queue declaration arguments. You can find information about them in the official RabbitMQ documentation: @@ -181,7 +183,8 @@ def __init__( if durable is EMPTY: durable = True elif not durable: - raise SetupError("Quorum and Stream queues must be durable") + error_msg = "Quorum and Stream queues must be durable" + raise SetupError(error_msg) elif durable is EMPTY: durable = False @@ -193,31 +196,10 @@ def __init__( self.bind_arguments = bind_arguments self.routing_key = routing_key self.robust = robust - self.passive = passive self.auto_delete = auto_delete self.arguments = {"x-queue-type": queue_type.value, **(arguments or {})} self.timeout = timeout - - if passive is not EMPTY: - warnings.warn( - DeprecationWarning( - "Use `declare` instead. Will be removed in the 0.6.0 release.", - ), - stacklevel=2, - ) - self.declare = not passive - else: - self.declare = declare - - def add_prefix(self, prefix: str) -> "RabbitQueue": - new_q: RabbitQueue = deepcopy(self) - - new_q.name = "".join((prefix, new_q.name)) - - if new_q.routing_key: - new_q.routing_key = "".join((prefix, new_q.routing_key)) - - return new_q + self.declare = declare CommonQueueArgs = TypedDict( @@ -289,18 +271,12 @@ class ClassicQueueArgs( ): """rabbitmq-server/deps/rabbit/src/rabbit_classic_queue.erl.""" - pass - class QuorumQueueArgs( CommonQueueArgs, SharedClassicAndQuorumQueueArgs, QuorumQueueSpecificArgs ): """rabbitmq-server/deps/rabbit/src/rabbit_quorum_queue.erl.""" - pass - class StreamQueueArgs(CommonQueueArgs, StreamQueueSpecificArgs): """rabbitmq-server/deps/rabbit/src/rabbit_stream_queue.erl.""" - - pass diff --git a/faststream/rabbit/schemas/reply.py b/faststream/rabbit/schemas/reply.py deleted file mode 100644 index 06acb377a9..0000000000 --- a/faststream/rabbit/schemas/reply.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Dict - -from typing_extensions import Annotated, Doc - - -class ReplyConfig: - """Class to store a config for subscribers' replies.""" - - __slots__ = ( - "immediate", - "mandatory", - "persist", - ) - - def __init__( - self, - mandatory: Annotated[ - bool, - Doc( - "Client waits for confirmation that the message is placed to some queue. " - "RabbitMQ returns message to client if there is no suitable queue." - ), - ] = True, - immediate: Annotated[ - bool, - Doc( - "Client expects that there is consumer ready to take the message to work. " - "RabbitMQ returns message to client if there is no suitable consumer." - ), - ] = False, - persist: Annotated[ - bool, - Doc("Restore the message on RabbitMQ reboot."), - ] = False, - ) -> None: - self.mandatory = mandatory - self.immediate = immediate - self.persist = persist - - def to_dict(self) -> Dict[str, bool]: - """Convert object to options dict.""" - return { - "mandatory": self.mandatory, - "immediate": self.immediate, - "persist": self.persist, - } diff --git a/faststream/rabbit/security.py b/faststream/rabbit/security.py index eb887076d6..4442583b2d 100644 --- a/faststream/rabbit/security.py +++ b/faststream/rabbit/security.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from faststream.security import ( BaseSecurity, @@ -6,19 +6,19 @@ ) if TYPE_CHECKING: - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict -def parse_security(security: Optional[BaseSecurity]) -> "AnyDict": +def parse_security(security: BaseSecurity | None) -> "AnyDict": """Convert security object to connection arguments.""" if security is None: return {} - elif isinstance(security, SASLPlaintext): + if isinstance(security, SASLPlaintext): return _parse_sasl_plaintext(security) - elif isinstance(security, BaseSecurity): + if isinstance(security, BaseSecurity): return _parse_base_security(security) - else: - raise NotImplementedError(f"RabbitBroker does not support {type(security)}") + msg = f"RabbitBroker does not support {type(security)}" + raise NotImplementedError(msg) def _parse_base_security(security: BaseSecurity) -> "AnyDict": diff --git a/faststream/rabbit/subscriber/__init__.py b/faststream/rabbit/subscriber/__init__.py index e69de29bb2..7d6576a632 100644 --- a/faststream/rabbit/subscriber/__init__.py +++ b/faststream/rabbit/subscriber/__init__.py @@ -0,0 +1,5 @@ +from .usecase import RabbitSubscriber + +__all__ = ( + "RabbitSubscriber", +) diff --git a/faststream/rabbit/subscriber/asyncapi.py b/faststream/rabbit/subscriber/asyncapi.py deleted file mode 100644 index 2ba7cabaa5..0000000000 --- a/faststream/rabbit/subscriber/asyncapi.py +++ /dev/null @@ -1,73 +0,0 @@ -from typing import Dict - -from faststream.asyncapi.schema import ( - Channel, - ChannelBinding, - CorrelationId, - Message, - Operation, - OperationBinding, -) -from faststream.asyncapi.schema.bindings import amqp -from faststream.asyncapi.utils import resolve_payloads -from faststream.rabbit.subscriber.usecase import LogicSubscriber -from faststream.rabbit.utils import is_routing_exchange - - -class AsyncAPISubscriber(LogicSubscriber): - """AsyncAPI-compatible Rabbit Subscriber class.""" - - def get_name(self) -> str: - return f"{self.queue.name}:{getattr(self.exchange, 'name', None) or '_'}:{self.call_name}" - - def get_schema(self) -> Dict[str, Channel]: - payloads = self.get_payloads() - - return { - self.name: Channel( - description=self.description, - subscribe=Operation( - bindings=OperationBinding( - amqp=amqp.OperationBinding( - cc=self.queue.routing, - ), - ) - 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, - ) - ), - } - ) - ), - ) - } diff --git a/faststream/rabbit/subscriber/config.py b/faststream/rabbit/subscriber/config.py new file mode 100644 index 0000000000..cf8d5d0221 --- /dev/null +++ b/faststream/rabbit/subscriber/config.py @@ -0,0 +1,51 @@ +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Optional + +from faststream._internal.configs import ( + SubscriberSpecificationConfig, + SubscriberUsecaseConfig, +) +from faststream._internal.constants import EMPTY +from faststream.middlewares import AckPolicy +from faststream.rabbit.configs.base import RabbitConfig, RabbitEndpointConfig + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict + from faststream.rabbit.schemas import Channel + + +@dataclass(kw_only=True) +class RabbitSubscriberSpecificationConfig( + RabbitConfig, + SubscriberSpecificationConfig, +): + pass + + +@dataclass(kw_only=True) +class RabbitSubscriberConfig(RabbitEndpointConfig, SubscriberUsecaseConfig): + consume_args: Optional["AnyDict"] = None + channel: Optional["Channel"] = None + + _no_ack: bool = field(default_factory=lambda: EMPTY, repr=False) + + @property + def ack_first(self) -> bool: + return self.__ack_policy is AckPolicy.ACK_FIRST + + @property + def ack_policy(self) -> AckPolicy: + if (policy := self.__ack_policy) is AckPolicy.ACK_FIRST: + return AckPolicy.DO_NOTHING + + return policy + + @property + def __ack_policy(self) -> AckPolicy: + if self._no_ack is not EMPTY and self._no_ack: + return AckPolicy.DO_NOTHING + + if self._ack_policy is EMPTY: + return AckPolicy.REJECT_ON_ERROR + + return self._ack_policy diff --git a/faststream/rabbit/subscriber/factory.py b/faststream/rabbit/subscriber/factory.py index 2d3bc9a2c9..487473956b 100644 --- a/faststream/rabbit/subscriber/factory.py +++ b/faststream/rabbit/subscriber/factory.py @@ -1,20 +1,26 @@ import warnings -from typing import TYPE_CHECKING, Iterable, Optional, Sequence, Union +from typing import TYPE_CHECKING, Optional -from faststream.rabbit.subscriber.asyncapi import AsyncAPISubscriber +from faststream._internal.constants import EMPTY +from faststream._internal.endpoint.subscriber.call_item import CallsCollection +from faststream.exceptions import SetupError -if TYPE_CHECKING: - from aio_pika import IncomingMessage - from fast_depends.dependencies import Depends +from .config import ( + RabbitSubscriberConfig, + RabbitSubscriberSpecificationConfig, +) +from .specification import RabbitSubscriberSpecification +from .usecase import RabbitSubscriber - from faststream.broker.types import BrokerMiddleware +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict + from faststream.middlewares import AckPolicy + from faststream.rabbit.configs import RabbitBrokerConfig from faststream.rabbit.schemas import ( Channel, RabbitExchange, RabbitQueue, - ReplyConfig, ) - from faststream.types import AnyDict def create_subscriber( @@ -22,42 +28,65 @@ def create_subscriber( queue: "RabbitQueue", exchange: "RabbitExchange", consume_args: Optional["AnyDict"], - reply_config: Optional["ReplyConfig"], channel: Optional["Channel"], # Subscriber args - no_ack: bool, no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[IncomingMessage]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], + ack_policy: "AckPolicy", + no_ack: bool, + # Broker args + config: "RabbitBrokerConfig", + # Specification args + title_: str | None, + description_: str | None, include_in_schema: bool, -) -> AsyncAPISubscriber: - if reply_config: # pragma: no cover - warnings.warn( - ( - "\n`reply_config` was deprecated in **FastStream 0.5.16**." - "\nPlease, use `RabbitResponse` object as a handler return instead." - "\nArgument will be removed in **FastStream 0.6.0**." - ), - DeprecationWarning, - stacklevel=2, - ) +) -> RabbitSubscriber: + _validate_input_for_misconfigure(ack_policy=ack_policy, no_ack=no_ack) - return AsyncAPISubscriber( - queue=queue, - exchange=exchange, + subscriber_config = RabbitSubscriberConfig( + no_reply=no_reply, consume_args=consume_args, - reply_config=reply_config, channel=channel, - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - title_=title_, - description_=description_, - include_in_schema=include_in_schema, + queue=queue, + exchange=exchange, + _ack_policy=ack_policy, + _no_ack=no_ack, + # broker + _outer_config=config, ) + + calls = CallsCollection() + + specification = RabbitSubscriberSpecification( + _outer_config=config, + specification_config=RabbitSubscriberSpecificationConfig( + title_=title_, + description_=description_, + include_in_schema=include_in_schema, + queue=queue, + exchange=exchange, + ), + calls=calls, + ) + + return RabbitSubscriber( + config=subscriber_config, + specification=specification, + calls=calls, + ) + + +def _validate_input_for_misconfigure( + *, + ack_policy: "AckPolicy", + no_ack: bool, +) -> None: + if no_ack is not EMPTY: + warnings.warn( + "`no_ack` option was deprecated in prior to `ack_policy=AckPolicy.ACK_FIRST`. Scheduled to remove in 0.7.0", + category=DeprecationWarning, + stacklevel=4, + ) + + if ack_policy is not EMPTY: + msg = "You can't use deprecated `no_ack` and `ack_policy` simultaneously. Please, use `ack_policy` only." + raise SetupError(msg) diff --git a/faststream/rabbit/subscriber/specification.py b/faststream/rabbit/subscriber/specification.py new file mode 100644 index 0000000000..c927c5da83 --- /dev/null +++ b/faststream/rabbit/subscriber/specification.py @@ -0,0 +1,69 @@ +from faststream._internal.endpoint.subscriber import SubscriberSpecification +from faststream.rabbit.configs import RabbitBrokerConfig +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 .config import RabbitSubscriberSpecificationConfig + + +class RabbitSubscriberSpecification(SubscriberSpecification[RabbitBrokerConfig, RabbitSubscriberSpecificationConfig]): + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + queue_name = self.config.queue.name + + exchange_name = getattr(self.config.exchange, "name", None) + + return f"{self._outer_config.prefix}{queue_name}:{exchange_name or '_'}:{self.call_name}" + + def get_schema(self) -> dict[str, SubscriberSpec]: + payloads = self.get_payloads() + + queue = self.config.queue.add_prefix(self._outer_config.prefix) + + exchange_binding = amqp.Exchange.from_exchange(self.config.exchange) + queue_binding = amqp.Queue.from_queue(queue) + + channel_name = self.name + + return { + channel_name: SubscriberSpec( + description=self.description, + operation=Operation( + bindings=OperationBinding( + amqp=amqp.OperationBinding( + routing_key=queue.routing(), + queue=queue_binding, + exchange=exchange_binding, + ack=True, + reply_to=None, + persist=None, + mandatory=None, + priority=None, + ), + ), + message=Message( + title=f"{channel_name}:Message", + payload=resolve_payloads(payloads), + ), + ), + bindings=ChannelBinding( + amqp=amqp.ChannelBinding( + virtual_host=self._outer_config.virtual_host, + queue=queue_binding, + exchange=exchange_binding, + ), + ), + ), + } diff --git a/faststream/rabbit/subscriber/usecase.py b/faststream/rabbit/subscriber/usecase.py index 3944fe0d11..eff3a6ee8f 100644 --- a/faststream/rabbit/subscriber/usecase.py +++ b/faststream/rabbit/subscriber/usecase.py @@ -1,168 +1,98 @@ -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - Optional, - Sequence, - Union, -) +import asyncio +import contextlib +from collections.abc import AsyncIterator, Sequence +from typing import TYPE_CHECKING, Any, Optional, cast import anyio from typing_extensions import override -from faststream.broker.publisher.fake import FakePublisher -from faststream.broker.subscriber.usecase import SubscriberUsecase -from faststream.broker.utils import process_msg -from faststream.exceptions import SetupError +from faststream._internal.endpoint.subscriber.usecase import SubscriberUsecase +from faststream._internal.endpoint.utils import process_msg from faststream.rabbit.parser import AioPikaParser -from faststream.rabbit.schemas import BaseRMQInformation +from faststream.rabbit.publisher.fake import RabbitFakePublisher if TYPE_CHECKING: from aio_pika import IncomingMessage, RobustQueue - from fast_depends.dependencies import Depends - from faststream.broker.message import StreamMessage - from faststream.broker.types import BrokerMiddleware, CustomCallable - from faststream.rabbit.helpers import RabbitDeclarer + from faststream._internal.endpoint.publisher import BasePublisherProto + from faststream._internal.endpoint.subscriber.call_item import CallsCollection + from faststream.message import StreamMessage + from faststream.rabbit.configs import RabbitBrokerConfig from faststream.rabbit.message import RabbitMessage - from faststream.rabbit.publisher.producer import AioPikaFastProducer - from faststream.rabbit.schemas import ( - Channel, - RabbitExchange, - RabbitQueue, - ReplyConfig, + from faststream.rabbit.schemas import RabbitExchange, RabbitQueue + + from .config import ( + RabbitSubscriberConfig, + RabbitSubscriberSpecificationConfig, ) - from faststream.types import AnyDict, Decorator, LoggerProto -class LogicSubscriber( - SubscriberUsecase["IncomingMessage"], - BaseRMQInformation, -): +class RabbitSubscriber(SubscriberUsecase["IncomingMessage"]): """A class to handle logic for RabbitMQ message consumption.""" - app_id: Optional[str] - declarer: Optional["RabbitDeclarer"] + app_id: str | None + _outer_config: "RabbitBrokerConfig" - _consumer_tag: Optional[str] + _consumer_tag: str | None _queue_obj: Optional["RobustQueue"] - _producer: Optional["AioPikaFastProducer"] - - def __init__( - self, - *, - queue: "RabbitQueue", - exchange: "RabbitExchange", - channel: Optional["Channel"], - consume_args: Optional["AnyDict"], - reply_config: Optional["ReplyConfig"], - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: Union[bool, int], - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[IncomingMessage]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - parser = AioPikaParser(pattern=queue.path_regex) + def __init__(self, config: "RabbitSubscriberConfig", specification: "RabbitSubscriberSpecificationConfig", calls: "CallsCollection") -> None: + parser = AioPikaParser(pattern=config.queue.path_regex) + config.decoder = parser.decode_message + config.parser = parser.parse_message super().__init__( - default_parser=parser.parse_message, - default_decoder=parser.decode_message, - # Propagated options - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, + config, + specification=specification, + calls=calls, ) - self.consume_args = consume_args or {} - self.reply_config = reply_config.to_dict() if reply_config else {} + self.queue = config.queue + self.exchange = config.exchange + + self.consume_args = config.consume_args or {} + + self.__no_ack = config.ack_first self._consumer_tag = None self._queue_obj = None - self.channel = channel + self.channel = config.channel - # BaseRMQInformation - self.queue = queue - self.exchange = exchange - # Setup it later - self.app_id = None - self.virtual_host = "" - self.declarer = None + @property + def app_id(self) -> str: + return self._outer_config.app_id - @override - def setup( # type: ignore[override] - self, - *, - app_id: Optional[str], - virtual_host: str, - declarer: "RabbitDeclarer", - # basic args - logger: Optional["LoggerProto"], - producer: Optional["AioPikaFastProducer"], - graceful_timeout: Optional[float], - extra_context: "AnyDict", - # broker options - broker_parser: Optional["CustomCallable"], - broker_decoder: Optional["CustomCallable"], - # dependant args - apply_types: bool, - is_validate: bool, - _get_dependant: Optional[Callable[..., Any]], - _call_decorators: Iterable["Decorator"], - ) -> None: - self.app_id = app_id - self.virtual_host = virtual_host - self.declarer = declarer - - super().setup( - logger=logger, - producer=producer, - graceful_timeout=graceful_timeout, - extra_context=extra_context, - broker_parser=broker_parser, - broker_decoder=broker_decoder, - apply_types=apply_types, - is_validate=is_validate, - _get_dependant=_get_dependant, - _call_decorators=_call_decorators, - ) + def routing(self) -> str: + return f"{self._outer_config.prefix}{self.queue.routing()}" @override async def start(self) -> None: """Starts the consumer for the RabbitMQ queue.""" - if self.declarer is None: - raise SetupError("You should setup subscriber at first.") + await super().start() - self._queue_obj = queue = await self.declarer.declare_queue( - self.queue, channel=self.channel + queue_to_bind = self.queue.add_prefix(self._outer_config.prefix) + + declarer = self._outer_config.declarer + + self._queue_obj = queue = await declarer.declare_queue( + queue_to_bind, + channel=self.channel, ) if ( self.exchange is not None - and not queue.passive # queue just getted from RMQ + and queue_to_bind.declare # queue just getted from RMQ and self.exchange.name # check Exchange is not default ): - exchange = await self.declarer.declare_exchange( - self.exchange, channel=self.channel + exchange = await declarer.declare_exchange( + self.exchange, + channel=self.channel, ) await queue.bind( exchange, - routing_key=self.queue.routing, - arguments=self.queue.bind_arguments, - timeout=self.queue.timeout, + routing_key=queue_to_bind.routing(), + arguments=queue_to_bind.bind_arguments, + timeout=queue_to_bind.timeout, robust=self.queue.robust, ) @@ -170,10 +100,11 @@ async def start(self) -> None: self._consumer_tag = await self._queue_obj.consume( # NOTE: aio-pika expects AbstractIncomingMessage, not IncomingMessage self.consume, # type: ignore[arg-type] + no_ack=self.__no_ack, arguments=self.consume_args, ) - await super().start() + self._post_start() async def close(self) -> None: await super().close() @@ -192,7 +123,7 @@ async def get_one( *, timeout: float = 5.0, no_ack: bool = True, - ) -> "Optional[RabbitMessage]": + ) -> "RabbitMessage | None": assert self._queue_obj, "You should start subscriber at first." # nosec B101 assert ( # nosec B101 not self.calls @@ -200,8 +131,11 @@ async def get_one( sleep_interval = timeout / 10 - raw_message: Optional[IncomingMessage] = None - with anyio.move_on_after(timeout): + raw_message: IncomingMessage | None = None + with ( + contextlib.suppress(asyncio.exceptions.CancelledError), + anyio.move_on_after(timeout), + ): while ( # noqa: ASYNC110 raw_message := await self._queue_obj.get( fail=False, @@ -211,49 +145,60 @@ async def get_one( ) is None: await anyio.sleep(sleep_interval) - msg: Optional[RabbitMessage] = await process_msg( # type: ignore[assignment] + context = self._outer_config.fd_config.context + + msg: RabbitMessage | None = await process_msg( # type: ignore[assignment] msg=raw_message, - middlewares=self._broker_middlewares, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), parser=self._parser, decoder=self._decoder, ) return msg + @override + async def __aiter__(self) -> AsyncIterator["RabbitMessage"]: + assert self._queue_obj, "You should start subscriber at first." # nosec B101 + assert ( # nosec B101 + not self.calls + ), "You can't use iterator method if subscriber has registered handlers." + + context = self._outer_config.fd_config.context + + async with self._queue_obj.iterator() as queue_iter: + async for raw_message in queue_iter: + raw_message = cast("IncomingMessage", raw_message) + + msg: RabbitMessage = await process_msg( # type: ignore[assignment] + msg=raw_message, + middlewares=( + m(raw_message, context=context) + for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + yield msg + def _make_response_publisher( self, message: "StreamMessage[Any]", - ) -> Sequence["FakePublisher"]: - if self._producer is None: - return () - + ) -> Sequence["BasePublisherProto"]: return ( - FakePublisher( - self._producer.publish, - publish_kwargs={ - **self.reply_config, - "routing_key": message.reply_to, - "app_id": self.app_id, - }, + RabbitFakePublisher( + self._outer_config.producer, + routing_key=message.reply_to, + app_id=self.app_id, ), ) - def __hash__(self) -> int: - return self.get_routing_hash(self.queue, self.exchange) - - @staticmethod - def get_routing_hash( - queue: "RabbitQueue", - exchange: Optional["RabbitExchange"] = None, - ) -> int: - """Calculate the routing hash for a RabbitMQ queue and exchange.""" - return hash(queue) + hash(exchange or "") - @staticmethod def build_log_context( message: Optional["StreamMessage[Any]"], queue: "RabbitQueue", exchange: Optional["RabbitExchange"] = None, - ) -> Dict[str, str]: + ) -> dict[str, str]: return { "queue": queue.name, "exchange": getattr(exchange, "name", ""), @@ -263,13 +208,9 @@ def build_log_context( def get_log_context( self, message: Optional["StreamMessage[Any]"], - ) -> Dict[str, str]: + ) -> dict[str, str]: return self.build_log_context( message=message, queue=self.queue, exchange=self.exchange, ) - - def add_prefix(self, prefix: str) -> None: - """Include Subscriber in router.""" - self.queue = self.queue.add_prefix(prefix) diff --git a/faststream/rabbit/testing.py b/faststream/rabbit/testing.py index 6863008cc1..1c9cb987a5 100644 --- a/faststream/rabbit/testing.py +++ b/faststream/rabbit/testing.py @@ -1,5 +1,6 @@ -from contextlib import contextmanager -from typing import TYPE_CHECKING, Any, Generator, Mapping, Optional, Tuple, Union +from collections.abc import Generator, Iterator, Mapping +from contextlib import ExitStack, contextmanager +from typing import TYPE_CHECKING, Any, Optional, Union from unittest import mock from unittest.mock import AsyncMock @@ -10,28 +11,28 @@ from pamqp.header import ContentHeader from typing_extensions import override -from faststream.broker.message import gen_cor_id -from faststream.broker.utils import resolve_custom_func -from faststream.exceptions import WRONG_PUBLISH_ARGS, SubscriberNotFound +from faststream._internal.endpoint.utils import resolve_custom_func +from faststream._internal.testing.broker import TestBroker, change_producer +from faststream.exceptions import SubscriberNotFound +from faststream.message import gen_cor_id from faststream.rabbit.broker.broker import RabbitBroker from faststream.rabbit.parser import AioPikaParser -from faststream.rabbit.publisher.asyncapi import AsyncAPIPublisher from faststream.rabbit.publisher.producer import AioPikaFastProducer from faststream.rabbit.schemas import ( ExchangeType, RabbitExchange, RabbitQueue, ) -from faststream.testing.broker import TestBroker -from faststream.utils.functions import timeout_scope if TYPE_CHECKING: - from aio_pika.abc import DateType, HeadersType, TimeoutType + from aio_pika.abc import DateType, HeadersType + from fast_depends.library.serializer import SerializerProto - from faststream.rabbit.subscriber.usecase import LogicSubscriber + from faststream.rabbit.publisher import RabbitPublisher + from faststream.rabbit.response import RabbitPublishCommand + from faststream.rabbit.subscriber import RabbitSubscriber from faststream.rabbit.types import AioPikaSendableMessage - __all__ = ("TestRabbitBroker",) @@ -39,32 +40,46 @@ class TestRabbitBroker(TestBroker[RabbitBroker]): """A class to test RabbitMQ brokers.""" @contextmanager - def _patch_broker(self, broker: RabbitBroker) -> Generator[None, None, None]: - with mock.patch.object( - broker, - "_channel", - new_callable=AsyncMock, - ), mock.patch.object( - broker, - "declarer", - new_callable=AsyncMock, - ), super()._patch_broker(broker): + def _patch_broker(self, broker: "RabbitBroker") -> Generator[None, None, None]: + with ( + mock.patch.object( + broker, + "_channel", + new_callable=AsyncMock, + ), + mock.patch.object( + broker.config, + "declarer", + new_callable=AsyncMock, + ), + super()._patch_broker(broker), + ): + yield + + @contextmanager + def _patch_producer(self, broker: RabbitBroker) -> Iterator[None]: + fake_producer = FakeProducer(broker) + + with ExitStack() as es: + es.enter_context( + change_producer(broker.config.broker_config, fake_producer) + ) yield @staticmethod - async def _fake_connect(broker: RabbitBroker, *args: Any, **kwargs: Any) -> None: - broker._producer = FakeProducer(broker) + async def _fake_connect(broker: "RabbitBroker", *args: Any, **kwargs: Any) -> None: + pass @staticmethod def create_publisher_fake_subscriber( - broker: RabbitBroker, - publisher: AsyncAPIPublisher, - ) -> Tuple["LogicSubscriber", bool]: - sub: Optional[LogicSubscriber] = None - for handler in broker._subscribers.values(): - if _is_handler_suitable( + broker: "RabbitBroker", + publisher: "RabbitPublisher", + ) -> tuple["RabbitSubscriber", bool]: + sub: RabbitSubscriber | None = None + for handler in broker.subscribers: + if _is_handler_matches( handler, - publisher.routing, + publisher.routing(), {}, publisher.exchange, ): @@ -74,7 +89,7 @@ def create_publisher_fake_subscriber( if sub is None: is_real = False sub = broker.subscriber( - queue=publisher.routing, + queue=publisher.routing(), exchange=publisher.exchange, ) else: @@ -93,15 +108,12 @@ class PatchedMessage(IncomingMessage): async def ack(self, multiple: bool = False) -> None: """Asynchronously acknowledge a message.""" - pass async def nack(self, multiple: bool = False, requeue: bool = True) -> None: """Nack the message.""" - pass async def reject(self, requeue: bool = False) -> None: """Rejects a task.""" - pass def build_message( @@ -111,25 +123,27 @@ def build_message( *, routing_key: str = "", persist: bool = False, - reply_to: Optional[str] = None, + reply_to: str | None = None, headers: Optional["HeadersType"] = None, - content_type: Optional[str] = None, - content_encoding: Optional[str] = None, - priority: Optional[int] = None, - correlation_id: Optional[str] = None, + content_type: str | None = None, + content_encoding: str | None = None, + priority: int | None = None, + correlation_id: str | None = None, expiration: Optional["DateType"] = None, - message_id: Optional[str] = None, + message_id: str | None = None, timestamp: Optional["DateType"] = None, - message_type: Optional[str] = None, - user_id: Optional[str] = None, - app_id: Optional[str] = None, + message_type: str | None = None, + user_id: str | None = None, + app_id: str | None = None, + serializer: Optional["SerializerProto"] = None ) -> PatchedMessage: """Build a patched RabbitMQ message for testing.""" que = RabbitQueue.validate(queue) exch = RabbitExchange.validate(exchange) - routing = routing_key or que.routing + routing = routing_key or que.routing() + correlation_id = correlation_id or gen_cor_id() msg = AioPikaParser.encode_message( message=message, persist=persist, @@ -140,11 +154,12 @@ def build_message( priority=priority, correlation_id=correlation_id, expiration=expiration, - message_id=message_id or gen_cor_id(), + message_id=message_id or correlation_id, timestamp=timestamp, message_type=message_type, user_id=user_id, app_id=app_id, + serializer=serializer ) return PatchedMessage( @@ -166,7 +181,7 @@ def build_message( message_type=message_type, user_id=msg.user_id, app_id=msg.app_id, - ) + ), ), body=msg.body, channel=AsyncMock(), @@ -186,130 +201,74 @@ def __init__(self, broker: RabbitBroker) -> None: default_parser = AioPikaParser() self._parser = resolve_custom_func(broker._parser, default_parser.parse_message) self._decoder = resolve_custom_func( - broker._decoder, default_parser.decode_message + broker._decoder, + default_parser.decode_message, ) @override async def publish( # type: ignore[override] self, - message: "AioPikaSendableMessage", - exchange: Union["RabbitExchange", str, None] = None, - *, - correlation_id: str = "", - routing_key: str = "", - mandatory: bool = True, - immediate: bool = False, - timeout: "TimeoutType" = None, - rpc: bool = False, - rpc_timeout: Optional[float] = 30.0, - raise_timeout: bool = False, - persist: bool = False, - reply_to: Optional[str] = None, - headers: Optional["HeadersType"] = None, - content_type: Optional[str] = None, - content_encoding: Optional[str] = None, - priority: Optional[int] = None, - expiration: Optional["DateType"] = None, - message_id: Optional[str] = None, - timestamp: Optional["DateType"] = None, - message_type: Optional[str] = None, - user_id: Optional[str] = None, - app_id: Optional[str] = None, - ) -> Optional[Any]: + cmd: "RabbitPublishCommand", + ) -> None: """Publish a message to a RabbitMQ queue or exchange.""" - exch = RabbitExchange.validate(exchange) - - if rpc and reply_to: - raise WRONG_PUBLISH_ARGS - incoming = build_message( - message=message, - exchange=exch, - routing_key=routing_key, - reply_to=reply_to, - app_id=app_id, - user_id=user_id, - message_type=message_type, - headers=headers, - persist=persist, - message_id=message_id, - priority=priority, - content_encoding=content_encoding, - content_type=content_type, - correlation_id=correlation_id, - expiration=expiration, - timestamp=timestamp, + message=cmd.body, + exchange=cmd.exchange, + routing_key=cmd.destination, + correlation_id=cmd.correlation_id, + headers=cmd.headers, + reply_to=cmd.reply_to, + serializer=self.broker.config.fd_config._serializer, + **cmd.message_options, ) - for handler in self.broker._subscribers.values(): # pragma: no branch - if _is_handler_suitable( - handler, incoming.routing_key, incoming.headers, exch + called = False + for handler in self.broker.subscribers: # pragma: no branch + if _is_handler_matches( + handler, + incoming.routing_key, + incoming.headers, + cmd.exchange, ): - with timeout_scope(rpc_timeout, raise_timeout): - response = await self._execute_handler(incoming, handler) - if rpc: - return await self._decoder(await self._parser(response)) + called = True + await self._execute_handler(incoming, handler) - return None + if not called: + raise SubscriberNotFound @override async def request( # type: ignore[override] self, - message: "AioPikaSendableMessage" = "", - exchange: Union["RabbitExchange", str, None] = None, - *, - correlation_id: str = "", - routing_key: str = "", - mandatory: bool = True, - immediate: bool = False, - timeout: Optional[float] = None, - persist: bool = False, - headers: Optional["HeadersType"] = None, - content_type: Optional[str] = None, - content_encoding: Optional[str] = None, - priority: Optional[int] = None, - expiration: Optional["DateType"] = None, - message_id: Optional[str] = None, - timestamp: Optional["DateType"] = None, - message_type: Optional[str] = None, - user_id: Optional[str] = None, - app_id: Optional[str] = None, + cmd: "RabbitPublishCommand", ) -> "PatchedMessage": """Publish a message to a RabbitMQ queue or exchange.""" - exch = RabbitExchange.validate(exchange) - incoming = build_message( - message=message, - exchange=exch, - routing_key=routing_key, - app_id=app_id, - user_id=user_id, - message_type=message_type, - headers=headers, - persist=persist, - message_id=message_id, - priority=priority, - content_encoding=content_encoding, - content_type=content_type, - correlation_id=correlation_id, - expiration=expiration, - timestamp=timestamp, + message=cmd.body, + exchange=cmd.exchange, + routing_key=cmd.destination, + correlation_id=cmd.correlation_id, + headers=cmd.headers, + **cmd.message_options, ) - for handler in self.broker._subscribers.values(): # pragma: no branch - if _is_handler_suitable( - handler, incoming.routing_key, incoming.headers, exch + for handler in self.broker.subscribers: # pragma: no branch + if _is_handler_matches( + handler, + incoming.routing_key, + incoming.headers, + cmd.exchange, ): - with anyio.fail_after(timeout): + with anyio.fail_after(cmd.timeout): return await self._execute_handler(incoming, handler) raise SubscriberNotFound async def _execute_handler( - self, msg: PatchedMessage, handler: "LogicSubscriber" + self, + msg: PatchedMessage, + handler: "RabbitSubscriber", ) -> "PatchedMessage": result = await handler.process_message(msg) - return build_message( routing_key=msg.routing_key, message=result.body, @@ -318,45 +277,47 @@ async def _execute_handler( ) -def _is_handler_suitable( - handler: "LogicSubscriber", +def _is_handler_matches( + handler: "RabbitSubscriber", routing_key: str, - headers: "Mapping[Any, Any]", - exchange: "RabbitExchange", + headers: Optional["Mapping[Any, Any]"] = None, + exchange: Optional["RabbitExchange"] = None, ) -> bool: + headers = headers or {} + exchange = RabbitExchange.validate(exchange) + if handler.exchange != exchange: return False if handler.exchange is None or handler.exchange.type == ExchangeType.DIRECT: - return handler.queue.name == routing_key + return handler.routing() == routing_key - elif handler.exchange.type == ExchangeType.FANOUT: + if handler.exchange.type == ExchangeType.FANOUT: return True - elif handler.exchange.type == ExchangeType.TOPIC: - return apply_pattern(handler.queue.routing, routing_key) + if handler.exchange.type == ExchangeType.TOPIC: + return apply_pattern(handler.routing(), routing_key) - elif handler.exchange.type == ExchangeType.HEADERS: + if handler.exchange.type == ExchangeType.HEADERS: queue_headers = (handler.queue.bind_arguments or {}).copy() if not queue_headers: return True - else: - match_rule = queue_headers.pop("x-match", "all") + match_rule = queue_headers.pop("x-match", "all") - full_match = True - is_headers_empty = True - for k, v in queue_headers.items(): - if headers.get(k) != v: - full_match = False - else: - is_headers_empty = False + full_match = True + is_headers_empty = True + for k, v in queue_headers.items(): + if headers.get(k) != v: + full_match = False + else: + is_headers_empty = False - if is_headers_empty: - return False + if is_headers_empty: + return False - return full_match or (match_rule == "any") + return full_match or (match_rule == "any") raise AssertionError @@ -371,7 +332,7 @@ def apply_pattern(pattern: str, current: str) -> bool: if (next_symb := next(current_queue, None)) is None: return False - elif pattern_symb == "#": + if pattern_symb == "#": next_pattern = next(pattern_queue, None) if next_pattern is None: @@ -391,7 +352,7 @@ def apply_pattern(pattern: str, current: str) -> bool: pattern_symb = next(pattern_queue, None) - elif pattern_symb == "*" or pattern_symb == next_symb: + elif pattern_symb in {"*", next_symb}: pattern_symb = next(pattern_queue, None) else: diff --git a/faststream/rabbit/types.py b/faststream/rabbit/types.py index fb5bca3e3c..1ec97cd210 100644 --- a/faststream/rabbit/types.py +++ b/faststream/rabbit/types.py @@ -1,8 +1,8 @@ -from typing import Union + +from typing import TypeAlias import aio_pika -from typing_extensions import TypeAlias -from faststream.types import SendableMessage +from faststream._internal.basic_types import SendableMessage -AioPikaSendableMessage: TypeAlias = Union[aio_pika.Message, SendableMessage] +AioPikaSendableMessage: TypeAlias = aio_pika.Message | SendableMessage diff --git a/faststream/rabbit/utils.py b/faststream/rabbit/utils.py index 131d0c9e84..2d72ed9212 100644 --- a/faststream/rabbit/utils.py +++ b/faststream/rabbit/utils.py @@ -12,25 +12,24 @@ def build_virtual_host( - url: Union[str, "URL", None], virtualhost: Optional[str], path: str + url: Union[str, "URL", None], virtualhost: str | None, path: str ) -> str: if (not url and not virtualhost) or virtualhost == "/": return "" - elif virtualhost: + if virtualhost: return virtualhost.replace("/", "", 1) - else: - return path.replace("/", "", 1) + return path.replace("/", "", 1) def build_url( url: Union[str, "URL", None] = None, *, - host: Optional[str] = None, - port: Optional[int] = None, - login: Optional[str] = None, - password: Optional[str] = None, - virtualhost: Optional[str] = None, - ssl: Optional[bool] = None, + host: str | None = None, + port: int | None = None, + login: str | None = None, + password: str | None = None, + virtualhost: str | None = None, + ssl: bool | None = None, ssl_options: Optional["SSLOptions"] = None, client_properties: Optional["RabbitClientProperties"] = None, **kwargs: Any, @@ -59,10 +58,10 @@ def build_url( def is_routing_exchange(exchange: Optional["RabbitExchange"]) -> bool: """Check if an exchange requires routing_key to deliver message.""" - return not exchange or exchange.type in ( + return not exchange or exchange.type in { ExchangeType.DIRECT.value, ExchangeType.TOPIC.value, - ) + } class RabbitClientProperties(TypedDict, total=False): diff --git a/faststream/redis/__init__.py b/faststream/redis/__init__.py index ccaaf6b276..0857c11df2 100644 --- a/faststream/redis/__init__.py +++ b/faststream/redis/__init__.py @@ -1,14 +1,16 @@ -try: - from faststream.testing.app import TestApp +from faststream._internal.testing.app import TestApp - from .annotations import Pipeline, Redis, RedisMessage - from .broker.broker import RedisBroker +try: + from .annotations import Pipeline, Redis, RedisMessage, RedisStreamMessage + from .broker import RedisBroker, RedisPublisher, RedisRoute, RedisRouter from .response import RedisResponse - from .router import RedisPublisher, RedisRoute, RedisRouter from .schemas import ListSub, PubSub, StreamSub from .testing import TestRedisBroker except ImportError as e: + if "'redis'" not in e.msg: + raise + from faststream.exceptions import INSTALL_FASTSTREAM_REDIS raise ImportError(INSTALL_FASTSTREAM_REDIS) from e @@ -24,6 +26,7 @@ "RedisResponse", "RedisRoute", "RedisRouter", + "RedisStreamMessage", "StreamSub", "TestApp", "TestRedisBroker", diff --git a/faststream/redis/annotations.py b/faststream/redis/annotations.py index 084dc5d376..aa4c845f37 100644 --- a/faststream/redis/annotations.py +++ b/faststream/redis/annotations.py @@ -1,15 +1,20 @@ -from typing import TYPE_CHECKING, AsyncIterator +from collections.abc import AsyncIterator +from typing import TYPE_CHECKING, Annotated -from redis.asyncio.client import Pipeline as _RedisPipeline -from redis.asyncio.client import Redis as _RedisClient -from typing_extensions import Annotated +from redis.asyncio.client import ( + Pipeline as _RedisPipeline, + Redis as _RedisClient, +) from faststream import Depends -from faststream.annotations import ContextRepo, Logger, NoCast +from faststream._internal.context import Context +from faststream.annotations import ContextRepo, Logger +from faststream.params import NoCast from faststream.redis.broker.broker import RedisBroker as RB -from faststream.redis.message import RedisStreamMessage as RSM -from faststream.redis.message import UnifyRedisMessage -from faststream.utils.context import Context +from faststream.redis.message import ( + RedisStreamMessage as RSM, + UnifyRedisMessage, +) if TYPE_CHECKING: RedisClient = _RedisClient[bytes] @@ -27,6 +32,7 @@ "Redis", "RedisBroker", "RedisMessage", + "RedisStreamMessage", ) RedisMessage = Annotated[UnifyRedisMessage, Context("message")] diff --git a/faststream/redis/broker/__init__.py b/faststream/redis/broker/__init__.py index e69de29bb2..e44bec34ae 100644 --- a/faststream/redis/broker/__init__.py +++ b/faststream/redis/broker/__init__.py @@ -0,0 +1,9 @@ +from .broker import RedisBroker +from .router import RedisPublisher, RedisRoute, RedisRouter + +__all__ = ( + "RedisBroker", + "RedisPublisher", + "RedisRoute", + "RedisRouter", +) diff --git a/faststream/redis/broker/broker.py b/faststream/redis/broker/broker.py index b73f07bb0b..d8d8c944ef 100644 --- a/faststream/redis/broker/broker.py +++ b/faststream/redis/broker/broker.py @@ -1,84 +1,83 @@ import logging -import warnings -from functools import partial +from collections.abc import Iterable, Mapping, Sequence from typing import ( TYPE_CHECKING, + Annotated, Any, - Callable, - Iterable, - Mapping, Optional, - Sequence, - Type, + TypeAlias, Union, ) from urllib.parse import urlparse import anyio from anyio import move_on_after -from redis.asyncio.client import Pipeline, Redis from redis.asyncio.connection import ( Connection, - ConnectionPool, DefaultParser, Encoder, parse_url, ) from redis.exceptions import ConnectionError -from typing_extensions import Annotated, Doc, TypeAlias, deprecated, override - -from faststream.__about__ import __version__ -from faststream.broker.message import gen_cor_id -from faststream.exceptions import NOT_CONNECTED_YET -from faststream.redis.broker.logging import RedisLoggingBroker -from faststream.redis.broker.registrator import RedisRegistrator +from typing_extensions import Doc, overload, override + +from faststream._internal.broker import BrokerUsecase +from faststream._internal.constants import EMPTY +from faststream._internal.di import FastDependsConfig +from faststream.message import gen_cor_id +from faststream.redis.configs import ConnectionState, RedisBrokerConfig +from faststream.redis.message import UnifyRedisDict from faststream.redis.publisher.producer import RedisFastProducer +from faststream.redis.response import RedisPublishCommand from faststream.redis.security import parse_security -from faststream.types import EMPTY +from faststream.response.publish_type import PublishType +from faststream.specification.schema import BrokerSpec + +from .logging import make_redis_logger_state +from .registrator import RedisRegistrator if TYPE_CHECKING: from types import TracebackType - from fast_depends.dependencies import Depends + from fast_depends.dependencies import Dependant + from fast_depends.library.serializer import SerializerProto + from redis.asyncio.client import Pipeline, Redis from redis.asyncio.connection import BaseParser - from typing_extensions import TypedDict, Unpack + from typing_extensions import TypedDict - from faststream.asyncapi import schema as asyncapi - from faststream.broker.types import ( + from faststream._internal.basic_types import ( + AnyDict, + LoggerProto, + SendableMessage, + ) + from faststream._internal.broker.abc_broker import Registrator + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, ) from faststream.redis.message import BaseMessage, RedisMessage from faststream.security import BaseSecurity - from faststream.types import ( - AnyDict, - AsyncFunc, - DecodedMessage, - Decorator, - LoggerProto, - SendableMessage, - ) + from faststream.specification.schema.extra import Tag, TagDict class RedisInitKwargs(TypedDict, total=False): - host: Optional[str] - port: Union[str, int, None] - db: Union[str, int, None] - client_name: Optional[str] - health_check_interval: Optional[float] - max_connections: Optional[int] - socket_timeout: Optional[float] - socket_connect_timeout: Optional[float] - socket_read_size: Optional[int] - socket_keepalive: Optional[bool] - socket_keepalive_options: Optional[Mapping[int, Union[int, bytes]]] - socket_type: Optional[int] - retry_on_timeout: Optional[bool] - encoding: Optional[str] - encoding_errors: Optional[str] - decode_responses: Optional[bool] - parser_class: Optional[Type["BaseParser"]] - connection_class: Optional[Type["Connection"]] - encoder_class: Optional[Type["Encoder"]] + host: str | None + port: str | int | None + db: str | int | None + client_name: str | None + health_check_interval: float | None + max_connections: int | None + socket_timeout: float | None + socket_connect_timeout: float | None + socket_read_size: int | None + socket_keepalive: bool | None + socket_keepalive_options: Mapping[int, int | bytes] | None + socket_type: int | None + retry_on_timeout: bool | None + encoding: str | None + encoding_errors: str | None + parser_class: type["BaseParser"] | None + connection_class: type["Connection"] | None + encoder_class: type["Encoder"] | None Channel: TypeAlias = str @@ -86,41 +85,40 @@ class RedisInitKwargs(TypedDict, total=False): class RedisBroker( RedisRegistrator, - RedisLoggingBroker, + BrokerUsecase[UnifyRedisDict, "Redis[bytes]"], ): """Redis broker.""" url: str - _producer: Optional[RedisFastProducer] + _producer: "RedisFastProducer" def __init__( self, url: str = "redis://localhost:6379", *, host: str = EMPTY, - port: Union[str, int] = EMPTY, - db: Union[str, int] = EMPTY, - connection_class: Type["Connection"] = EMPTY, - client_name: Optional[str] = None, + port: str | int = EMPTY, + db: str | int = EMPTY, + connection_class: type["Connection"] = EMPTY, + client_name: str | None = None, health_check_interval: float = 0, - max_connections: Optional[int] = None, - socket_timeout: Optional[float] = None, - socket_connect_timeout: Optional[float] = None, + max_connections: int | None = None, + socket_timeout: float | None = None, + socket_connect_timeout: float | None = None, socket_read_size: int = 65536, socket_keepalive: bool = False, - socket_keepalive_options: Optional[Mapping[int, Union[int, bytes]]] = None, + socket_keepalive_options: Mapping[int, int | bytes] | None = None, socket_type: int = 0, retry_on_timeout: bool = False, encoding: str = "utf-8", encoding_errors: str = "strict", - decode_responses: bool = False, - parser_class: Type["BaseParser"] = DefaultParser, - encoder_class: Type["Encoder"] = Encoder, + parser_class: type["BaseParser"] = DefaultParser, + encoder_class: type["Encoder"] = Encoder, # broker args graceful_timeout: Annotated[ - Optional[float], + float | None, Doc( - "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down." + "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down.", ), ] = 15.0, decoder: Annotated[ @@ -132,40 +130,44 @@ def __init__( Doc("Custom parser object."), ] = None, dependencies: Annotated[ - Iterable["Depends"], + Iterable["Dependant"], Doc("Dependencies to apply to all broker subscribers."), ] = (), middlewares: Annotated[ Sequence["BrokerMiddleware[BaseMessage]"], Doc("Middlewares to apply to all broker publishers/subscribers."), ] = (), + routers: Annotated[ + Sequence["Registrator[BaseMessage]"], + Doc("Routers to apply to broker."), + ] = (), # AsyncAPI args security: Annotated[ Optional["BaseSecurity"], Doc( - "Security options to connect broker and generate AsyncAPI server security information." + "Security options to connect broker and generate AsyncAPI server security information.", ), ] = None, - asyncapi_url: Annotated[ - Optional[str], + specification_url: Annotated[ + str | None, Doc("AsyncAPI hardcoded server addresses. Use `servers` if not specified."), ] = None, protocol: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server protocol."), ] = None, protocol_version: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server protocol version."), ] = "custom", description: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server description."), ] = None, tags: Annotated[ - Optional[Iterable[Union["asyncapi.Tag", "asyncapi.TagDict"]]], + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ Optional["LoggerProto"], @@ -175,43 +177,23 @@ def __init__( int, Doc("Service messages log level."), ] = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - Doc("Default logger log format."), - ] = EMPTY, # FastDepends args apply_types: Annotated[ bool, Doc("Whether to use FastDepends or not."), ] = True, - validate: Annotated[ - bool, - Doc("Whether to cast types using Pydantic validation."), - ] = True, - _get_dependant: Annotated[ - Optional[Callable[..., Any]], - Doc("Custom library dependant generator callback."), - ] = None, - _call_decorators: Annotated[ - Iterable["Decorator"], - Doc("Any custom decorator to apply to wrapped functions."), - ] = (), + serializer: Optional["SerializerProto"] = EMPTY, ) -> None: - self._producer = None - - if asyncapi_url is None: - asyncapi_url = url + if specification_url is None: + specification_url = url if protocol is None: - url_kwargs = urlparse(asyncapi_url) + url_kwargs = urlparse(specification_url) protocol = url_kwargs.scheme - super().__init__( - url=url, + connection_options = _resolve_url_options( + url, + security=security, host=host, port=port, db=db, @@ -227,248 +209,143 @@ def __init__( retry_on_timeout=retry_on_timeout, encoding=encoding, encoding_errors=encoding_errors, - decode_responses=decode_responses, parser_class=parser_class, connection_class=connection_class, encoder_class=encoder_class, - # Basic args - # broker base - graceful_timeout=graceful_timeout, - dependencies=dependencies, - decoder=decoder, - parser=parser, - middlewares=middlewares, - # AsyncAPI - description=description, - asyncapi_url=asyncapi_url, - protocol=protocol, - protocol_version=protocol_version, - security=security, - tags=tags, - # logging - logger=logger, - log_level=log_level, - log_fmt=log_fmt, - # FastDepends args - apply_types=apply_types, - validate=validate, - _get_dependant=_get_dependant, - _call_decorators=_call_decorators, ) - @override - async def connect( # type: ignore[override] - self, - url: Optional[str] = EMPTY, - **kwargs: "Unpack[RedisInitKwargs]", - ) -> "Redis[bytes]": - """Connect to the Redis server.""" - if url is not EMPTY or kwargs: - warnings.warn( - "`RedisBroker().connect(...) options were " - "deprecated in **FastStream 0.5.40**. " - "Please, use `RedisBroker(...)` instead. " - "All these options will be removed in **FastStream 0.6.0**.", - DeprecationWarning, - stacklevel=2, - ) - - if url is not EMPTY: - connect_kwargs: AnyDict = { - "url": url, - **kwargs, - } - else: - connect_kwargs = dict(kwargs).copy() - - return await super().connect(**connect_kwargs) + connection_state = ConnectionState(connection_options) - @override - async def _connect( # type: ignore[override] - self, - url: str, - *, - host: str, - port: Union[str, int], - db: Union[str, int], - connection_class: Type["Connection"], - client_name: Optional[str], - health_check_interval: float, - max_connections: Optional[int], - socket_timeout: Optional[float], - socket_connect_timeout: Optional[float], - socket_read_size: int, - socket_keepalive: bool, - socket_keepalive_options: Optional[Mapping[int, Union[int, bytes]]], - socket_type: int, - retry_on_timeout: bool, - encoding: str, - encoding_errors: str, - decode_responses: bool, - parser_class: Type["BaseParser"], - encoder_class: Type["Encoder"], - ) -> "Redis[bytes]": - url_options: AnyDict = { - **dict(parse_url(url)), - **parse_security(self.security), - "client_name": client_name, - "health_check_interval": health_check_interval, - "max_connections": max_connections, - "socket_timeout": socket_timeout, - "socket_connect_timeout": socket_connect_timeout, - "socket_read_size": socket_read_size, - "socket_keepalive": socket_keepalive, - "socket_keepalive_options": socket_keepalive_options, - "socket_type": socket_type, - "retry_on_timeout": retry_on_timeout, - "encoding": encoding, - "encoding_errors": encoding_errors, - "decode_responses": decode_responses, - "parser_class": parser_class, - "encoder_class": encoder_class, - } - - if port is not EMPTY: - url_options["port"] = port - if host is not EMPTY: - url_options["host"] = host - if db is not EMPTY: - url_options["db"] = db - if connection_class is not EMPTY: - url_options["connection_class"] = connection_class - - pool = ConnectionPool( - **url_options, - lib_name="faststream", - lib_version=__version__, + super().__init__( + **connection_options, + routers=routers, + config=RedisBrokerConfig( + connection=connection_state, + producer=RedisFastProducer( + connection=connection_state, + parser=parser, + decoder=decoder, + ), + # both args + broker_middlewares=middlewares, + broker_parser=parser, + broker_decoder=decoder, + logger=make_redis_logger_state( + logger=logger, + log_level=log_level, + ), + fd_config=FastDependsConfig( + use_fastdepends=apply_types, + serializer=serializer, + ), + # subscriber args + broker_dependencies=dependencies, + graceful_timeout=graceful_timeout, + extra_context={ + "broker": self, + }, + ), + specification=BrokerSpec( + description=description, + url=[specification_url], + protocol=protocol, + protocol_version=protocol_version, + security=security, + tags=tags, + ), ) - client: Redis[bytes] = Redis.from_pool(pool) # type: ignore[attr-defined] - self._producer = RedisFastProducer( - connection=client, - parser=self._parser, - decoder=self._decoder, - ) - return client + @override + async def _connect(self) -> "Redis[bytes]": + await self.config.connect() + return self.config.connection.client - async def _close( + async def close( self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, + exc_type: type[BaseException] | None = None, + exc_val: BaseException | None = None, exc_tb: Optional["TracebackType"] = None, ) -> None: - if self._connection is not None: - await self._connection.aclose() # type: ignore[attr-defined] - - await super()._close(exc_type, exc_val, exc_tb) + await super().close(exc_type, exc_val, exc_tb) + await self.config.disconnect() + self._connection = None async def start(self) -> None: + await self.connect() await super().start() - for handler in self._subscribers.values(): - self._log( - f"`{handler.call_name}` waiting for messages", - extra=handler.get_log_context(None), - ) - await handler.start() - - @property - def _subscriber_setup_extra(self) -> "AnyDict": - return { - **super()._subscriber_setup_extra, - "connection": self._connection, - } + @overload + async def publish( + self, + message: "SendableMessage" = None, + channel: str | None = None, + *, + reply_to: str = "", + headers: Optional["AnyDict"] = None, + correlation_id: str | None = None, + list: str | None = None, + stream: None = None, + maxlen: int | None = None, + ) -> int: ... + + @overload + async def publish( + self, + message: "SendableMessage" = None, + channel: str | None = None, + *, + reply_to: str = "", + headers: Optional["AnyDict"] = None, + correlation_id: str | None = None, + list: str | None = None, + stream: str, + maxlen: int | None = None, + ) -> bytes: ... @override - async def publish( # type: ignore[override] + async def publish( self, - message: Annotated[ - "SendableMessage", - Doc("Message body to send."), - ] = None, - channel: Annotated[ - Optional[str], - Doc("Redis PubSub object name to send message."), - ] = None, + message: "SendableMessage" = None, + channel: str | None = None, *, - reply_to: Annotated[ - str, - Doc("Reply message destination PubSub object name."), - ] = "", - headers: Annotated[ - Optional["AnyDict"], - Doc("Message headers to store metainformation."), - ] = None, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - list: Annotated[ - Optional[str], - Doc("Redis List object name to send message."), - ] = None, - stream: Annotated[ - Optional[str], - Doc("Redis Stream object name to send message."), - ] = None, - maxlen: Annotated[ - Optional[int], - Doc( - "Redis Stream maxlen publish option. " - "Remove eldest message if maxlen exceeded." - ), - ] = None, - rpc: Annotated[ - bool, - Doc("Whether to wait for reply in blocking mode."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - rpc_timeout: Annotated[ - Optional[float], - Doc("RPC reply waiting time."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method with `timeout` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = 30.0, - raise_timeout: Annotated[ - bool, - Doc( - "Whetever to raise `TimeoutError` or return `None` at **rpc_timeout**. " - "RPC request returns `None` at timeout by default." - ), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "`request` always raises TimeoutError instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - pipeline: Annotated[ - Optional["Pipeline[bytes]"], - Doc( - "Optional Redis `Pipeline` object to batch multiple commands. " - "Use it to group Redis operations for optimized execution and reduced latency." - ), - ] = None, - ) -> Optional["DecodedMessage"]: + reply_to: str = "", + headers: Optional["AnyDict"] = None, + correlation_id: str | None = None, + list: str | None = None, + stream: str | None = None, + maxlen: int | None = None, + pipeline: Optional["Pipeline[bytes]"] = None, + ) -> int | bytes: """Publish message directly. - This method allows you to publish message in not AsyncAPI-documented way. You can use it in another frameworks - applications or to publish messages from time to time. - - Please, use `@broker.publisher(...)` or `broker.publisher(...).publish(...)` instead in a regular way. + This method allows you to publish a message in a non-AsyncAPI-documented way. + It can be used in other frameworks or to publish messages at specific intervals. + + Args: + message: + Message body to send. + channel: + Redis PubSub object name to send message. + reply_to: + Reply message destination PubSub object name. + headers: + Message headers to store metainformation. + correlation_id: + Manual message correlation_id setter. correlation_id is a useful option to trace messages. + list: + Redis List object name to send message. + stream: + Redis Stream object name to send message. + maxlen: + Redis Stream maxlen publish option. Remove eldest message if maxlen exceeded. + pipeline: + Redis pipeline to use for publishing messages. + + Returns: + int: The result of the publish operation, typically the number of messages published. """ - return await super().publish( + cmd = RedisPublishCommand( message, - producer=self._producer, correlation_id=correlation_id or gen_cor_id(), channel=channel, list=list, @@ -476,28 +353,27 @@ async def publish( # type: ignore[override] maxlen=maxlen, reply_to=reply_to, headers=headers, - rpc=rpc, - rpc_timeout=rpc_timeout, - raise_timeout=raise_timeout, + _publish_type=PublishType.PUBLISH, pipeline=pipeline, ) + return await super()._basic_publish(cmd, producer=self.config.producer) + @override async def request( # type: ignore[override] self, message: "SendableMessage", - channel: Optional[str] = None, + channel: str | None = None, *, - list: Optional[str] = None, - stream: Optional[str] = None, - maxlen: Optional[int] = None, - correlation_id: Optional[str] = None, + list: str | None = None, + stream: str | None = None, + maxlen: int | None = None, + correlation_id: str | None = None, headers: Optional["AnyDict"] = None, - timeout: Optional[float] = 30.0, + timeout: float | None = 30.0, ) -> "RedisMessage": - msg: RedisMessage = await super().request( + cmd = RedisPublishCommand( message, - producer=self._producer, correlation_id=correlation_id or gen_cor_id(), channel=channel, list=list, @@ -505,53 +381,49 @@ async def request( # type: ignore[override] maxlen=maxlen, headers=headers, timeout=timeout, + _publish_type=PublishType.REQUEST, + ) + msg: RedisMessage = await super()._basic_request( + cmd, producer=self.config.producer ) return msg async def publish_batch( self, - *msgs: Annotated[ - "SendableMessage", - Doc("Messages bodies to send."), - ], - list: Annotated[ - str, - Doc("Redis List object name to send messages."), - ], - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - pipeline: Annotated[ - Optional["Pipeline[bytes]"], - Doc( - "Optional Redis `Pipeline` object to batch multiple commands. " - "Use it to group Redis operations for optimized execution and reduced latency." - ), - ] = None, - ) -> None: - """Publish multiple messages to Redis List by one request.""" - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - correlation_id = correlation_id or gen_cor_id() - - call: AsyncFunc = self._producer.publish_batch - - for m in self._middlewares[::-1]: - call = partial(m(None).publish_scope, call) - - await call( - *msgs, + *messages: "SendableMessage", + list: str, + correlation_id: str | None = None, + reply_to: str = "", + headers: Optional["AnyDict"] = None, + pipeline: Optional["Pipeline[bytes]"] = None, + ) -> int: + """Publish multiple messages to Redis List by one request. + + Args: + *messages: Messages bodies to send. + list: Redis List object name to send messages. + correlation_id: Manual message **correlation_id** setter. **correlation_id** is a useful option to trace messages. + reply_to: Reply message destination PubSub object name. + headers: Message headers to store metainformation. + pipeline: Redis pipeline to use for publishing messages. + + Returns: + int: The result of the batch publish operation. + """ + cmd = RedisPublishCommand( + *messages, list=list, - correlation_id=correlation_id, + reply_to=reply_to, + headers=headers, + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.PUBLISH, pipeline=pipeline, ) + return await self._basic_publish_batch(cmd, producer=self.config.producer) + @override - async def ping(self, timeout: Optional[float]) -> bool: + async def ping(self, timeout: float | None) -> bool: sleep_time = (timeout or 10) / 10 with move_on_after(timeout) as cancel_scope: @@ -572,3 +444,16 @@ async def ping(self, timeout: Optional[float]) -> bool: await anyio.sleep(sleep_time) return False + + +def _resolve_url_options( + url: str, + *, + security: Optional["BaseSecurity"], + **kwargs: Any, +) -> "AnyDict": + return { + **dict(parse_url(url)), + **parse_security(security), + **{k: v for k, v in kwargs.items() if v is not EMPTY}, + } diff --git a/faststream/redis/broker/logging.py b/faststream/redis/broker/logging.py index 440a37c528..5832c7fc18 100644 --- a/faststream/redis/broker/logging.py +++ b/faststream/redis/broker/logging.py @@ -1,67 +1,63 @@ import logging -from typing import TYPE_CHECKING, Any, ClassVar, Optional +from functools import partial +from typing import TYPE_CHECKING -from typing_extensions import Annotated, deprecated - -from faststream.broker.core.usecase import BrokerUsecase -from faststream.log.logging import get_broker_logger -from faststream.redis.message import UnifyRedisDict -from faststream.types import EMPTY +from faststream._internal.logger import ( + DefaultLoggerStorage, + make_logger_state, +) +from faststream._internal.logger.logging import get_broker_logger if TYPE_CHECKING: - from redis.asyncio.client import Redis # noqa: F401 + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.context import ContextRepo + - from faststream.types import LoggerProto +class RedisParamsStorage(DefaultLoggerStorage): + def __init__(self) -> None: + super().__init__() + self._max_channel_name = 4 -class RedisLoggingBroker(BrokerUsecase[UnifyRedisDict, "Redis[bytes]"]): - """A class that extends the LoggingMixin class and adds additional functionality for logging Redis related information.""" + self.logger_log_level = logging.INFO - _max_channel_name: int - __max_msg_id_ln: ClassVar[int] = 10 + def set_level(self, level: int) -> None: + self.logger_log_level = level - def __init__( - self, - *args: Any, - logger: Optional["LoggerProto"] = EMPTY, - log_level: int = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." + def register_subscriber(self, params: "AnyDict") -> None: + self._max_channel_name = max( + ( + self._max_channel_name, + len(params.get("channel", "")), ), - ] = EMPTY, - **kwargs: Any, - ) -> None: - super().__init__( - *args, - logger=logger, - # TODO: generate unique logger names to not share between brokers - default_logger=get_broker_logger( + ) + + def get_logger(self, *, context: "ContextRepo") -> "LoggerProto": + message_id_ln = 10 + + # TODO: generate unique logger names to not share between brokers + if not (lg := self._get_logger_ref()): + lg = get_broker_logger( name="redis", default_context={ "channel": "", }, - message_id_ln=self.__max_msg_id_ln, - ), - log_level=log_level, - log_fmt=log_fmt, - **kwargs, - ) - self._max_channel_name = 4 - - def get_fmt(self) -> str: - return ( - "%(asctime)s %(levelname)-8s - " - f"%(channel)-{self._max_channel_name}s | " - f"%(message_id)-{self.__max_msg_id_ln}s " - "- %(message)s" - ) - - def _setup_log_context( - self, - *, - channel: Optional[str] = None, - ) -> None: - self._max_channel_name = max((self._max_channel_name, len(channel or ""))) + message_id_ln=message_id_ln, + fmt=( + "%(asctime)s %(levelname)-8s - " + f"%(channel)-{self._max_channel_name}s | " + f"%(message_id)-{message_id_ln}s " + "- %(message)s" + ), + context=context, + log_level=self.logger_log_level, + ) + self._logger_ref.add(lg) + + return lg + + +make_redis_logger_state = partial( + make_logger_state, + default_storage_cls=RedisParamsStorage, +) diff --git a/faststream/redis/broker/registrator.py b/faststream/redis/broker/registrator.py index 578ddac894..9221c661ad 100644 --- a/faststream/redis/broker/registrator.py +++ b/faststream/redis/broker/registrator.py @@ -1,36 +1,41 @@ -from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Sequence, Union, cast +from collections.abc import Iterable, Sequence +from typing import TYPE_CHECKING, Annotated, Any, Optional, Union, cast -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import Doc, deprecated, override -from faststream.broker.core.abc import ABCBroker -from faststream.broker.utils import default_filter +from faststream._internal.broker.abc_broker import Registrator +from faststream._internal.constants import EMPTY from faststream.exceptions import SetupError +from faststream.middlewares import AckPolicy from faststream.redis.message import UnifyRedisDict -from faststream.redis.publisher.asyncapi import AsyncAPIPublisher -from faststream.redis.subscriber.asyncapi import AsyncAPISubscriber +from faststream.redis.publisher.factory import create_publisher from faststream.redis.subscriber.factory import SubsciberType, create_subscriber if TYPE_CHECKING: - from fast_depends.dependencies import Depends + from fast_depends.dependencies import Dependant - from faststream.broker.types import ( + from faststream._internal.basic_types import AnyDict + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, - Filter, PublisherMiddleware, SubscriberMiddleware, ) + from faststream.redis.configs import RedisBrokerConfig from faststream.redis.message import UnifyRedisMessage - from faststream.redis.publisher.asyncapi import PublisherType + from faststream.redis.publisher.specification import ( + PublisherType, + SpecificationPublisher, + ) from faststream.redis.schemas import ListSub, PubSub, StreamSub - from faststream.types import AnyDict -class RedisRegistrator(ABCBroker[UnifyRedisDict]): +class RedisRegistrator(Registrator[UnifyRedisDict]): """Includable to RedisBroker router.""" - _subscribers: Dict[int, "SubsciberType"] - _publishers: Dict[int, "PublisherType"] + config: "RedisBrokerConfig" + _subscribers: list["SubsciberType"] + _publishers: list["PublisherType"] @override def subscriber( # type: ignore[override] @@ -50,13 +55,13 @@ def subscriber( # type: ignore[override] ] = None, # broker arguments dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), + Iterable["Dependant"], + Doc("Dependencies list (`[Dependant(),]`) to apply to the subscriber."), ] = (), parser: Annotated[ Optional["CustomCallable"], Doc( - "Parser to map original **aio_pika.IncomingMessage** Msg to FastStream one." + "Parser to map original **aio_pika.IncomingMessage** Msg to FastStream one.", ), ] = None, decoder: Annotated[ @@ -65,78 +70,67 @@ def subscriber( # type: ignore[override] ] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[UnifyRedisMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[UnifyRedisMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = default_filter, - retry: Annotated[ + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), + no_ack: Annotated[ bool, - Doc("Whether to `nack` message at processing exception."), + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), deprecated( - "Deprecated in **FastStream 0.5.40**." - "Please, manage acknowledgement policy manually." - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" ), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, # AsyncAPI information title: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ bool, Doc("Whetever to include operation in AsyncAPI schema or not."), ] = True, - ) -> AsyncAPISubscriber: - subscriber = cast( - "AsyncAPISubscriber", - super().subscriber( - create_subscriber( - channel=channel, - list=list, - stream=stream, - # subscriber args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=self._middlewares, - broker_dependencies=self._dependencies, - # AsyncAPI - title_=title, - description_=description, - include_in_schema=self._solve_include_in_schema(include_in_schema), - ) - ), + max_workers: Annotated[ + int, + Doc("Number of workers to process messages concurrently."), + ] = 1, + ): + subscriber = create_subscriber( + channel=channel, + list=list, + stream=stream, + # subscriber args + max_workers=max_workers, + no_ack=no_ack, + no_reply=no_reply, + ack_policy=ack_policy, + config=self.config, + # AsyncAPI + title_=title, + description_=description, + include_in_schema=include_in_schema, ) + subscriber = super().subscriber(subscriber) # type: ignore[assignment] + return subscriber.add_call( - filter_=filter, parser_=parser or self._parser, decoder_=decoder or self._decoder, dependencies_=dependencies, @@ -163,7 +157,7 @@ def publisher( # type: ignore[override] Optional["AnyDict"], Doc( "Message headers to store metainformation. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -172,29 +166,33 @@ def publisher( # type: ignore[override] ] = "", middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), # AsyncAPI information title: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI publisher object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, Doc("Whetever to include operation in AsyncAPI schema or not."), ] = True, - ) -> AsyncAPIPublisher: + ) -> "SpecificationPublisher": """Creates long-living and AsyncAPI-documented publisher object. You can use it as a handler decorator (handler should be decorated by `@broker.subscriber(...)` too) - `@broker.publisher(...)`. @@ -203,23 +201,23 @@ def publisher( # type: ignore[override] Or you can create a publisher object to call it lately - `broker.publisher(...).publish(...)`. """ return cast( - "AsyncAPIPublisher", + "SpecificationPublisher", super().publisher( - AsyncAPIPublisher.create( + create_publisher( channel=channel, list=list, stream=stream, headers=headers, reply_to=reply_to, # Specific - broker_middlewares=self._middlewares, + config=self.config, middlewares=middlewares, # AsyncAPI title_=title, description_=description, schema_=schema, - include_in_schema=self._solve_include_in_schema(include_in_schema), - ) + include_in_schema=include_in_schema, + ), ), ) @@ -229,9 +227,9 @@ def include_router( router: "RedisRegistrator", # type: ignore[override] *, prefix: str = "", - dependencies: Iterable["Depends"] = (), + dependencies: Iterable["Dependant"] = (), middlewares: Iterable["BrokerMiddleware[UnifyRedisDict]"] = (), - include_in_schema: Optional[bool] = None, + include_in_schema: bool | None = None, ) -> None: if not isinstance(router, RedisRegistrator): msg = ( diff --git a/faststream/redis/broker/router.py b/faststream/redis/broker/router.py new file mode 100644 index 0000000000..d5729d5ee0 --- /dev/null +++ b/faststream/redis/broker/router.py @@ -0,0 +1,268 @@ +from collections.abc import Awaitable, Callable, Iterable, Sequence +from typing import TYPE_CHECKING, Annotated, Any, Optional, Union + +from typing_extensions import Doc, deprecated + +from faststream._internal.broker.router import ( + ArgsContainer, + BrokerRouter, + SubscriberRoute, +) +from faststream._internal.constants import EMPTY +from faststream.middlewares import AckPolicy +from faststream.redis.configs.broker import RedisRouterConfig +from faststream.redis.message import BaseMessage + +from .registrator import RedisRegistrator + +if TYPE_CHECKING: + from fast_depends.dependencies import Dependant + + from faststream._internal.basic_types import AnyDict, SendableMessage + from faststream._internal.broker.abc_broker import Registrator + from faststream._internal.types import ( + BrokerMiddleware, + CustomCallable, + PublisherMiddleware, + SubscriberMiddleware, + ) + from faststream.redis.message import UnifyRedisMessage + from faststream.redis.schemas import ListSub, PubSub, StreamSub + + +class RedisPublisher(ArgsContainer): + """Delayed RedisPublisher registration object. + + Just a copy of RedisRegistrator.publisher(...) arguments. + """ + + def __init__( + self, + channel: Annotated[ + Union["PubSub", str, None], + Doc("Redis PubSub object name to send message."), + ] = None, + *, + list: Annotated[ + Union["ListSub", str, None], + Doc("Redis List object name to send message."), + ] = None, + stream: Annotated[ + Union["StreamSub", str, None], + Doc("Redis Stream object name to send message."), + ] = None, + headers: Annotated[ + Optional["AnyDict"], + Doc( + "Message headers to store metainformation. " + "Can be overridden by `publish.headers` if specified.", + ), + ] = None, + reply_to: Annotated[ + str, + Doc("Reply message destination PubSub object name."), + ] = "", + middlewares: Annotated[ + Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + Doc("Publisher middlewares to wrap outgoing messages."), + ] = (), + # AsyncAPI information + title: Annotated[ + str | None, + Doc("AsyncAPI publisher object title."), + ] = None, + description: Annotated[ + str | None, + Doc("AsyncAPI publisher object description."), + ] = None, + schema: Annotated[ + Any | None, + Doc( + "AsyncAPI publishing message type. " + "Should be any python-native object annotation or `pydantic.BaseModel`.", + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = True, + ) -> None: + super().__init__( + channel=channel, + list=list, + stream=stream, + headers=headers, + reply_to=reply_to, + middlewares=middlewares, + title=title, + description=description, + schema=schema, + include_in_schema=include_in_schema, + ) + + +class RedisRoute(SubscriberRoute): + """Class to store delayed RedisBroker subscriber registration.""" + + def __init__( + self, + call: Annotated[ + Callable[..., "SendableMessage"] | Callable[..., Awaitable["SendableMessage"]], + Doc( + "Message handler function " + "to wrap the same with `@broker.subscriber(...)` way.", + ), + ], + channel: Annotated[ + Union["PubSub", str, None], + Doc("Redis PubSub object name to send message."), + ] = None, + *, + publishers: Annotated[ + Iterable["RedisPublisher"], + Doc("Redis publishers to broadcast the handler result."), + ] = (), + list: Annotated[ + Union["ListSub", str, None], + Doc("Redis List object name to send message."), + ] = None, + stream: Annotated[ + Union["StreamSub", str, None], + Doc("Redis Stream object name to send message."), + ] = None, + # broker arguments + dependencies: Annotated[ + Iterable["Dependant"], + Doc("Dependencies list (`[Dependant(),]`) to apply to the subscriber."), + ] = (), + parser: Annotated[ + Optional["CustomCallable"], + Doc( + "Parser to map original **aio_pika.IncomingMessage** Msg to FastStream one.", + ), + ] = None, + decoder: Annotated[ + Optional["CustomCallable"], + Doc("Function to decode FastStream msg bytes body to python objects."), + ] = None, + middlewares: Annotated[ + Sequence["SubscriberMiddleware[UnifyRedisMessage]"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), + no_ack: Annotated[ + bool, + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), + deprecated( + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" + ), + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, + no_reply: Annotated[ + bool, + Doc( + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", + ), + ] = False, + # AsyncAPI information + title: Annotated[ + str | None, + Doc("AsyncAPI subscriber object title."), + ] = None, + description: Annotated[ + str | None, + Doc( + "AsyncAPI subscriber object description. " + "Uses decorated docstring as default.", + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = True, + max_workers: Annotated[ + int, + Doc("Number of workers to process messages concurrently."), + ] = 1, + ) -> None: + super().__init__( + call, + channel=channel, + publishers=publishers, + list=list, + stream=stream, + dependencies=dependencies, + max_workers=max_workers, + parser=parser, + decoder=decoder, + middlewares=middlewares, + ack_policy=ack_policy, + no_ack=no_ack, + no_reply=no_reply, + title=title, + description=description, + include_in_schema=include_in_schema, + ) + + +class RedisRouter(RedisRegistrator, BrokerRouter[BaseMessage]): + """Includable to RedisBroker router.""" + + def __init__( + self, + prefix: Annotated[ + str, + Doc("String prefix to add to all subscribers queues."), + ] = "", + handlers: Annotated[ + Iterable[RedisRoute], + Doc("Route object to include."), + ] = (), + *, + dependencies: Annotated[ + Iterable["Dependant"], + Doc( + "Dependencies list (`[Dependant(),]`) to apply to all routers' publishers/subscribers.", + ), + ] = (), + middlewares: Annotated[ + Sequence["BrokerMiddleware[BaseMessage]"], + Doc("Router middlewares to apply to all routers' publishers/subscribers."), + ] = (), + routers: Annotated[ + Sequence["Registrator[BaseMessage]"], + Doc("Routers to apply to broker."), + ] = (), + parser: Annotated[ + Optional["CustomCallable"], + Doc("Parser to map original **IncomingMessage** Msg to FastStream one."), + ] = None, + decoder: Annotated[ + Optional["CustomCallable"], + Doc("Function to decode FastStream msg bytes body to python objects."), + ] = None, + include_in_schema: Annotated[ + bool | None, + Doc("Whetever to include operation in AsyncAPI schema or not."), + ] = None, + ) -> None: + super().__init__( + handlers=handlers, + config=RedisRouterConfig( + prefix=prefix, + broker_dependencies=dependencies, + broker_middlewares=middlewares, + broker_parser=parser, + broker_decoder=decoder, + include_in_schema=include_in_schema, + ), + routers=routers, + ) diff --git a/faststream/redis/configs/__init__.py b/faststream/redis/configs/__init__.py new file mode 100644 index 0000000000..c61fdecc0c --- /dev/null +++ b/faststream/redis/configs/__init__.py @@ -0,0 +1,7 @@ +from .broker import RedisBrokerConfig +from .state import ConnectionState + +__all__ = ( + "ConnectionState", + "RedisBrokerConfig", +) diff --git a/faststream/redis/configs/broker.py b/faststream/redis/configs/broker.py new file mode 100644 index 0000000000..219dfbdea3 --- /dev/null +++ b/faststream/redis/configs/broker.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from faststream._internal.configs import BrokerConfig +from faststream.exceptions import IncorrectState + +if TYPE_CHECKING: + from faststream.redis.publisher.producer import RedisFastProducer + + from .state import ConnectionState + + +@dataclass(kw_only=True) +class RedisBrokerConfig(BrokerConfig): + producer: "RedisFastProducer" + connection: "ConnectionState" + + async def connect(self) -> None: + self.producer.connect(self.fd_config._serializer) + await self.connection.connect() + + async def disconnect(self) -> None: + await self.connection.disconnect() + + +@dataclass(kw_only=True) +class RedisRouterConfig(BrokerConfig): + @property + def connection(self) -> ConnectionError: + raise IncorrectState diff --git a/faststream/redis/configs/state.py b/faststream/redis/configs/state.py new file mode 100644 index 0000000000..99fb3e5499 --- /dev/null +++ b/faststream/redis/configs/state.py @@ -0,0 +1,46 @@ +from typing import Any + +from redis.asyncio.client import Redis +from redis.asyncio.connection import ConnectionPool + +from faststream.__about__ import __version__ +from faststream.exceptions import IncorrectState + + +class ConnectionState: + def __init__(self, options: dict[str, Any] | None = None) -> None: + self._options = options or {} + + self._connected = False + self._client: Redis[bytes] | None = None + + @property + def client(self) -> "Redis[bytes]": + if not self._client: + msg = "Connection is not available yet. Please, connect the broker first." + raise IncorrectState(msg) + + return self._client + + def __bool__(self) -> bool: + return self._connected + + async def connect(self) -> "Redis[bytes]": + pool = ConnectionPool( + **self._options, + lib_name="faststream", + lib_version=__version__, + ) + client: Redis[bytes] = Redis.from_pool(pool) + + self._client = client + self._connected = True + + return client + + async def disconnect(self) -> None: + if self._client: + await self._client.aclose() + + self._client = None + self._connected = False diff --git a/faststream/redis/fastapi/__init__.py b/faststream/redis/fastapi/__init__.py index db8b797dda..117c03aae2 100644 --- a/faststream/redis/fastapi/__init__.py +++ b/faststream/redis/fastapi/__init__.py @@ -1,11 +1,13 @@ +from typing import Annotated + from redis.asyncio.client import Redis as RedisClient -from typing_extensions import Annotated -from faststream.broker.fastapi.context import Context, ContextRepo, Logger +from faststream._internal.fastapi.context import Context, ContextRepo, Logger from faststream.redis.broker.broker import RedisBroker as RB -from faststream.redis.fastapi.fastapi import RedisRouter from faststream.redis.message import BaseMessage as RM # noqa: N814 +from .fastapi import RedisRouter + __all__ = ( "Context", "ContextRepo", diff --git a/faststream/redis/fastapi/fastapi.py b/faststream/redis/fastapi/fastapi.py index db0186f538..a09bd010fb 100644 --- a/faststream/redis/fastapi/fastapi.py +++ b/faststream/redis/fastapi/fastapi.py @@ -1,17 +1,11 @@ import logging +from collections.abc import Callable, Iterable, Mapping, Sequence from typing import ( TYPE_CHECKING, + Annotated, Any, - Callable, - Dict, - Iterable, - List, - Mapping, Optional, - Sequence, - Type, Union, - cast, ) from fastapi.datastructures import Default @@ -24,17 +18,15 @@ ) from starlette.responses import JSONResponse from starlette.routing import BaseRoute -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import Doc, deprecated, override from faststream.__about__ import SERVICE_NAME -from faststream.broker.fastapi.router import StreamRouter -from faststream.broker.utils import default_filter +from faststream._internal.constants import EMPTY +from faststream._internal.fastapi.router import StreamRouter +from faststream.middlewares import AckPolicy from faststream.redis.broker.broker import RedisBroker as RB from faststream.redis.message import UnifyRedisDict -from faststream.redis.publisher.asyncapi import AsyncAPIPublisher from faststream.redis.schemas import ListSub, PubSub, StreamSub -from faststream.redis.subscriber.asyncapi import AsyncAPISubscriber -from faststream.types import EMPTY if TYPE_CHECKING: from enum import Enum @@ -45,17 +37,17 @@ from starlette.responses import Response from starlette.types import ASGIApp, Lifespan - from faststream.asyncapi import schema as asyncapi - from faststream.broker.types import ( + from faststream._internal.basic_types import AnyDict, LoggerProto + from faststream._internal.types import ( BrokerMiddleware, CustomCallable, - Filter, PublisherMiddleware, SubscriberMiddleware, ) from faststream.redis.message import UnifyRedisMessage + from faststream.redis.publisher.specification import SpecificationPublisher from faststream.security import BaseSecurity - from faststream.types import AnyDict, LoggerProto + from faststream.specification.schema.extra import Tag, TagDict class RedisRouter(StreamRouter[UnifyRedisDict]): @@ -69,29 +61,28 @@ def __init__( url: str = "redis://localhost:6379", *, host: str = EMPTY, - port: Union[str, int] = EMPTY, - db: Union[str, int] = EMPTY, - connection_class: Type["Connection"] = EMPTY, - client_name: Optional[str] = SERVICE_NAME, + port: str | int = EMPTY, + db: str | int = EMPTY, + connection_class: type["Connection"] = EMPTY, + client_name: str | None = SERVICE_NAME, health_check_interval: float = 0, - max_connections: Optional[int] = None, - socket_timeout: Optional[float] = None, - socket_connect_timeout: Optional[float] = None, + max_connections: int | None = None, + socket_timeout: float | None = None, + socket_connect_timeout: float | None = None, socket_read_size: int = 65536, socket_keepalive: bool = False, - socket_keepalive_options: Optional[Mapping[int, Union[int, bytes]]] = None, + socket_keepalive_options: Mapping[int, int | bytes] | None = None, socket_type: int = 0, retry_on_timeout: bool = False, encoding: str = "utf-8", encoding_errors: str = "strict", - decode_responses: bool = False, - parser_class: Type["BaseParser"] = DefaultParser, - encoder_class: Type["Encoder"] = Encoder, + parser_class: type["BaseParser"] = DefaultParser, + encoder_class: type["Encoder"] = Encoder, # broker base args graceful_timeout: Annotated[ - Optional[float], + float | None, Doc( - "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down." + "Graceful shutdown timeout. Broker waits for all running subscribers completion before shut down.", ), ] = 15.0, decoder: Annotated[ @@ -110,58 +101,50 @@ def __init__( security: Annotated[ Optional["BaseSecurity"], Doc( - "Security options to connect broker and generate AsyncAPI server security information." + "Security options to connect broker and generate AsyncAPI server security information.", ), ] = None, - asyncapi_url: Annotated[ - Optional[str], + specification_url: Annotated[ + str | None, Doc("AsyncAPI hardcoded server addresses. Use `servers` if not specified."), ] = None, protocol: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server protocol."), ] = None, protocol_version: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server protocol version."), ] = "custom", description: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI server description."), ] = None, - asyncapi_tags: Annotated[ - Optional[Iterable[Union["asyncapi.Tag", "asyncapi.TagDict"]]], + specification_tags: Annotated[ + Iterable[Union["Tag", "TagDict"]], Doc("AsyncAPI server tags."), - ] = None, + ] = (), # logging args logger: Annotated[ - Union["LoggerProto", None, object], + Optional["LoggerProto"], Doc("User specified logger to pass into Context and log service messages."), ] = EMPTY, log_level: Annotated[ int, Doc("Service messages log level."), ] = logging.INFO, - log_fmt: Annotated[ - Optional[str], - deprecated( - "Argument `log_fmt` is deprecated since 0.5.42 and will be removed in 0.6.0. " - "Pass a pre-configured `logger` instead." - ), - Doc("Default logger log format."), - ] = EMPTY, # StreamRouter options setup_state: Annotated[ bool, Doc( "Whether to add broker to app scope in lifespan. " - "You should disable this option at old ASGI servers." + "You should disable this option at old ASGI servers.", ), ] = True, schema_url: Annotated[ - Optional[str], + str | None, Doc( - "AsyncAPI schema url. You should set this option to `None` to disable AsyncAPI routes at all." + "AsyncAPI schema url. You should set this option to `None` to disable AsyncAPI routes at all.", ), ] = "/asyncapi", # FastAPI args @@ -170,7 +153,7 @@ def __init__( Doc("An optional path prefix for the router."), ] = "", tags: Annotated[ - Optional[List[Union[str, "Enum"]]], + list[Union[str, "Enum"]] | None, Doc( """ A list of tags to be applied to all the *path operations* in this @@ -180,11 +163,11 @@ def __init__( Read more about it in the [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). - """ + """, ), ] = None, dependencies: Annotated[ - Optional[Sequence["params.Depends"]], + Sequence["params.Depends"] | None, Doc( """ A list of dependencies (using `Depends()`) to be applied to all the @@ -192,22 +175,22 @@ def __init__( Read more about it in the [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). - """ + """, ), ] = None, default_response_class: Annotated[ - Type["Response"], + type["Response"], Doc( """ The default response class to be used. Read more in the [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). - """ + """, ), ] = Default(JSONResponse), responses: Annotated[ - Optional[Dict[Union[int, str], "AnyDict"]], + dict[int | str, "AnyDict"] | None, Doc( """ Additional responses to be shown in OpenAPI. @@ -219,11 +202,11 @@ def __init__( And in the [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). - """ + """, ), ] = None, callbacks: Annotated[ - Optional[List[BaseRoute]], + list[BaseRoute] | None, Doc( """ OpenAPI callbacks that should apply to all *path operations* in this @@ -233,11 +216,11 @@ def __init__( Read more about it in the [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). - """ + """, ), ] = None, routes: Annotated[ - Optional[List[BaseRoute]], + list[BaseRoute] | None, Doc( """ **Note**: you probably shouldn't use this parameter, it is inherited @@ -246,7 +229,7 @@ def __init__( --- A list of routes to serve incoming HTTP and WebSocket requests. - """ + """, ), deprecated( """ @@ -255,7 +238,7 @@ def __init__( In FastAPI, you normally would use the *path operation methods*, like `router.get()`, `router.post()`, etc. - """ + """, ), ] = None, redirect_slashes: Annotated[ @@ -264,7 +247,7 @@ def __init__( """ Whether to detect and redirect slashes in URLs when the client doesn't use the same format. - """ + """, ), ] = True, default: Annotated[ @@ -273,33 +256,33 @@ def __init__( """ Default function handler for this router. Used to handle 404 Not Found errors. - """ + """, ), ] = None, dependency_overrides_provider: Annotated[ - Optional[Any], + Any | None, Doc( """ Only used internally by FastAPI to handle dependency overrides. You shouldn't need to use it. It normally points to the `FastAPI` app object. - """ + """, ), ] = None, route_class: Annotated[ - Type["APIRoute"], + type["APIRoute"], Doc( """ Custom route (*path operation*) class to be used by this router. Read more about it in the [FastAPI docs for Custom Request and APIRoute class](https://fastapi.tiangolo.com/how-to/custom-request-and-route/#custom-apiroute-class-in-a-router). - """ + """, ), ] = APIRoute, on_startup: Annotated[ - Optional[Sequence[Callable[[], Any]]], + Sequence[Callable[[], Any]] | None, Doc( """ A list of startup event handler functions. @@ -307,11 +290,11 @@ def __init__( You should instead use the `lifespan` handlers. Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ + """, ), ] = None, on_shutdown: Annotated[ - Optional[Sequence[Callable[[], Any]]], + Sequence[Callable[[], Any]] | None, Doc( """ A list of shutdown event handler functions. @@ -320,7 +303,7 @@ def __init__( Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ + """, ), ] = None, lifespan: Annotated[ @@ -332,11 +315,11 @@ def __init__( Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). - """ + """, ), ] = None, deprecated: Annotated[ - Optional[bool], + bool | None, Doc( """ Mark all *path operations* in this router as deprecated. @@ -345,7 +328,7 @@ def __init__( Read more about it in the [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). - """ + """, ), ] = None, include_in_schema: Annotated[ @@ -359,7 +342,7 @@ def __init__( Read more about it in the [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). - """ + """, ), ] = True, generate_unique_id_function: Annotated[ @@ -374,7 +357,7 @@ def __init__( Read more about it in the [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). - """ + """, ), ] = Default(generate_unique_id), ) -> None: @@ -393,7 +376,6 @@ def __init__( retry_on_timeout=retry_on_timeout, encoding=encoding, encoding_errors=encoding_errors, - decode_responses=decode_responses, parser_class=parser_class, connection_class=connection_class, encoder_class=encoder_class, @@ -408,14 +390,13 @@ def __init__( # logger options logger=logger, log_level=log_level, - log_fmt=log_fmt, # AsyncAPI options security=security, protocol=protocol, description=description, protocol_version=protocol_version, - asyncapi_tags=asyncapi_tags, - asyncapi_url=asyncapi_url, + specification_tags=specification_tags, + specification_url=specification_url, # FastAPI kwargs prefix=prefix, tags=tags, @@ -440,16 +421,16 @@ def __init__( def subscriber( # type: ignore[override] self, channel: Annotated[ - Union[str, PubSub, None], + str | PubSub | None, Doc("Redis PubSub object name to send message."), ] = None, *, list: Annotated[ - Union[str, ListSub, None], + str | ListSub | None, Doc("Redis List object name to send message."), ] = None, stream: Annotated[ - Union[str, StreamSub, None], + str | StreamSub | None, Doc("Redis Stream object name to send message."), ] = None, # broker arguments @@ -460,7 +441,7 @@ def subscriber( # type: ignore[override] parser: Annotated[ Optional["CustomCallable"], Doc( - "Parser to map original **aio_pika.IncomingMessage** Msg to FastStream one." + "Parser to map original **aio_pika.IncomingMessage** Msg to FastStream one.", ), ] = None, decoder: Annotated[ @@ -469,43 +450,37 @@ def subscriber( # type: ignore[override] ] = None, middlewares: Annotated[ Sequence["SubscriberMiddleware[UnifyRedisMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[UnifyRedisMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, + Doc("Subscriber middlewares to wrap incoming message processing."), + ] = (), no_ack: Annotated[ bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, + Doc("Whether to disable **FastStream** auto acknowledgement logic or not."), + deprecated( + "This option was deprecated in 0.6.0 to prior to **ack_policy=AckPolicy.DO_NOTHING**. " + "Scheduled to remove in 0.7.0" + ), + ] = EMPTY, + ack_policy: AckPolicy = EMPTY, no_reply: Annotated[ bool, Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." + "Whether to disable **FastStream** RPC and Reply To auto responses or not.", ), ] = False, # AsyncAPI information title: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI subscriber object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc( "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." + "Uses decorated docstring as default.", ), ] = None, include_in_schema: Annotated[ @@ -544,7 +519,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). - """ + """, ), ] = Default(None), response_model_include: Annotated[ @@ -556,7 +531,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_exclude: Annotated[ @@ -568,7 +543,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = None, response_model_by_alias: Annotated[ @@ -580,7 +555,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). - """ + """, ), ] = True, response_model_exclude_unset: Annotated[ @@ -598,7 +573,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_defaults: Annotated[ @@ -615,7 +590,7 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). - """ + """, ), ] = False, response_model_exclude_none: Annotated[ @@ -632,58 +607,59 @@ def subscriber( # type: ignore[override] Read more about it in the [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). - """ + """, ), ] = False, - ) -> AsyncAPISubscriber: - return cast( - "AsyncAPISubscriber", - super().subscriber( - channel=channel, - list=list, - stream=stream, - dependencies=dependencies, - parser=parser, - decoder=decoder, - middlewares=middlewares, - filter=filter, - retry=retry, - no_ack=no_ack, - no_reply=no_reply, - title=title, - description=description, - include_in_schema=include_in_schema, - # FastAPI args - response_model=response_model, - response_model_include=response_model_include, - response_model_exclude=response_model_exclude, - response_model_by_alias=response_model_by_alias, - response_model_exclude_unset=response_model_exclude_unset, - response_model_exclude_defaults=response_model_exclude_defaults, - response_model_exclude_none=response_model_exclude_none, - ), + max_workers: Annotated[ + int, + Doc("Number of workers to process messages concurrently."), + ] = 1, + ): + return super().subscriber( + channel=channel, + max_workers=max_workers, + list=list, + stream=stream, + dependencies=dependencies, + parser=parser, + decoder=decoder, + middlewares=middlewares, + ack_policy=ack_policy, + no_ack=no_ack, + no_reply=no_reply, + title=title, + description=description, + include_in_schema=include_in_schema, + # FastAPI args + response_model=response_model, + response_model_include=response_model_include, + response_model_exclude=response_model_exclude, + response_model_by_alias=response_model_by_alias, + response_model_exclude_unset=response_model_exclude_unset, + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, ) @override def publisher( self, channel: Annotated[ - Union[str, PubSub, None], + str | PubSub | None, Doc("Redis PubSub object name to send message."), ] = None, list: Annotated[ - Union[str, ListSub, None], + str | ListSub | None, Doc("Redis List object name to send message."), ] = None, stream: Annotated[ - Union[str, StreamSub, None], + str | StreamSub | None, Doc("Redis Stream object name to send message."), ] = None, headers: Annotated[ Optional["AnyDict"], Doc( "Message headers to store metainformation. " - "Can be overridden by `publish.headers` if specified." + "Can be overridden by `publish.headers` if specified.", ), ] = None, reply_to: Annotated[ @@ -692,29 +668,33 @@ def publisher( ] = "", middlewares: Annotated[ Sequence["PublisherMiddleware"], + deprecated( + "This option was deprecated in 0.6.0. Use router-level middlewares instead." + "Scheduled to remove in 0.7.0" + ), Doc("Publisher middlewares to wrap outgoing messages."), ] = (), # AsyncAPI information title: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI publisher object title."), ] = None, description: Annotated[ - Optional[str], + str | None, Doc("AsyncAPI publisher object description."), ] = None, schema: Annotated[ - Optional[Any], + Any | None, Doc( "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." + "Should be any python-native object annotation or `pydantic.BaseModel`.", ), ] = None, include_in_schema: Annotated[ bool, Doc("Whetever to include operation in AsyncAPI schema or not."), ] = True, - ) -> AsyncAPIPublisher: + ) -> "SpecificationPublisher": return self.broker.publisher( channel, list=list, diff --git a/faststream/redis/message.py b/faststream/redis/message.py index 22d4258305..0b4dcd06d2 100644 --- a/faststream/redis/message.py +++ b/faststream/redis/message.py @@ -1,21 +1,20 @@ from typing import ( TYPE_CHECKING, - Dict, - List, Literal, Optional, + TypeAlias, TypeVar, Union, ) -from typing_extensions import NotRequired, TypeAlias, TypedDict, override +from typing_extensions import NotRequired, TypedDict, override -from faststream.broker.message import StreamMessage as BrokerStreamMessage +from faststream.message import StreamMessage as BrokerStreamMessage if TYPE_CHECKING: from redis.asyncio import Redis - from faststream.types import DecodedMessage + from faststream._internal.basic_types import DecodedMessage BaseMessage: TypeAlias = Union[ @@ -37,13 +36,8 @@ class UnifyRedisDict(TypedDict): "bstream", ] channel: str - data: Union[ - bytes, - List[bytes], - Dict[bytes, bytes], - List[Dict[bytes, bytes]], - ] - pattern: NotRequired[Optional[bytes]] + data: bytes | list[bytes] | dict[bytes, bytes] | list[dict[bytes, bytes]] + pattern: NotRequired[bytes | None] class UnifyRedisMessage(BrokerStreamMessage[UnifyRedisDict]): @@ -56,65 +50,63 @@ class PubSubMessage(TypedDict): type: Literal["pmessage", "message"] channel: str data: bytes - pattern: Optional[bytes] + pattern: bytes | None class RedisMessage(BrokerStreamMessage[PubSubMessage]): pass -class ListMessage(TypedDict): +class _ListMessage(TypedDict): """A class to represent an Abstract List message.""" channel: str -class DefaultListMessage(ListMessage): +class DefaultListMessage(_ListMessage): """A class to represent a single List message.""" type: Literal["list"] data: bytes -class BatchListMessage(ListMessage): +class BatchListMessage(_ListMessage): """A class to represent a List messages batch.""" type: Literal["blist"] - data: List[bytes] + data: list[bytes] class RedisListMessage(BrokerStreamMessage[DefaultListMessage]): """StreamMessage for single List message.""" - pass - class RedisBatchListMessage(BrokerStreamMessage[BatchListMessage]): """StreamMessage for single List message.""" - decoded_body: List["DecodedMessage"] + decoded_body: list["DecodedMessage"] DATA_KEY = "__data__" bDATA_KEY = DATA_KEY.encode() # noqa: N816 -class StreamMessage(TypedDict): +class _StreamMessage(TypedDict): channel: str - message_ids: List[bytes] + message_ids: list[bytes] -class DefaultStreamMessage(StreamMessage): +class DefaultStreamMessage(_StreamMessage): type: Literal["stream"] - data: Dict[bytes, bytes] + data: dict[bytes, bytes] -class BatchStreamMessage(StreamMessage): +class BatchStreamMessage(_StreamMessage): type: Literal["bstream"] - data: List[Dict[bytes, bytes]] + data: list[dict[bytes, bytes]] -_StreamMsgType = TypeVar("_StreamMsgType", bound=StreamMessage) +_StreamMsgType = TypeVar("_StreamMsgType", bound=_StreamMessage) class _RedisStreamMessageMixin(BrokerStreamMessage[_StreamMsgType]): @@ -122,7 +114,7 @@ class _RedisStreamMessageMixin(BrokerStreamMessage[_StreamMsgType]): async def ack( self, redis: Optional["Redis[bytes]"] = None, - group: Optional[str] = None, + group: str | None = None, ) -> None: if not self.committed and group is not None and redis is not None: ids = self.raw_message["message_ids"] @@ -134,7 +126,7 @@ async def ack( async def nack( self, redis: Optional["Redis[bytes]"] = None, - group: Optional[str] = None, + group: str | None = None, ) -> None: await super().nack() @@ -142,7 +134,7 @@ async def nack( async def reject( self, redis: Optional["Redis[bytes]"] = None, - group: Optional[str] = None, + group: str | None = None, ) -> None: await super().reject() @@ -158,4 +150,4 @@ class RedisStreamMessage(_RedisStreamMessageMixin[DefaultStreamMessage]): class RedisBatchStreamMessage(_RedisStreamMessageMixin[BatchStreamMessage]): - decoded_body: List["DecodedMessage"] + decoded_body: list["DecodedMessage"] diff --git a/faststream/redis/opentelemetry/middleware.py b/faststream/redis/opentelemetry/middleware.py index 54c0024143..6f011c3140 100644 --- a/faststream/redis/opentelemetry/middleware.py +++ b/faststream/redis/opentelemetry/middleware.py @@ -1,19 +1,19 @@ -from typing import Optional from opentelemetry.metrics import Meter, MeterProvider from opentelemetry.trace import TracerProvider from faststream.opentelemetry.middleware import TelemetryMiddleware from faststream.redis.opentelemetry.provider import RedisTelemetrySettingsProvider +from faststream.redis.response import RedisPublishCommand -class RedisTelemetryMiddleware(TelemetryMiddleware): +class RedisTelemetryMiddleware(TelemetryMiddleware[RedisPublishCommand]): def __init__( self, *, - tracer_provider: Optional[TracerProvider] = None, - meter_provider: Optional[MeterProvider] = None, - meter: Optional[Meter] = None, + tracer_provider: TracerProvider | None = None, + meter_provider: MeterProvider | None = None, + meter: Meter | None = None, ) -> None: super().__init__( settings_provider_factory=lambda _: RedisTelemetrySettingsProvider(), diff --git a/faststream/redis/opentelemetry/provider.py b/faststream/redis/opentelemetry/provider.py index 4144ba2ee5..334cc2f564 100644 --- a/faststream/redis/opentelemetry/provider.py +++ b/faststream/redis/opentelemetry/provider.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Sized, cast +from typing import TYPE_CHECKING, cast from opentelemetry.semconv.trace import SpanAttributes @@ -6,8 +6,9 @@ from faststream.opentelemetry.consts import MESSAGING_DESTINATION_PUBLISH_NAME if TYPE_CHECKING: - from faststream.broker.message import StreamMessage - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict + from faststream.message import StreamMessage + from faststream.response import PublishCommand class RedisTelemetrySettingsProvider(TelemetrySettingsProvider["AnyDict"]): @@ -30,7 +31,7 @@ def get_consume_attrs_from_message( if cast("str", msg.raw_message.get("type", "")).startswith("b"): attrs[SpanAttributes.MESSAGING_BATCH_MESSAGE_COUNT] = len( - cast("Sized", msg._decoded_body) + msg.raw_message["data"] ) return attrs @@ -41,21 +42,21 @@ def get_consume_destination_name( ) -> str: return self._get_destination(msg.raw_message) - def get_publish_attrs_from_kwargs( + def get_publish_attrs_from_cmd( self, - kwargs: "AnyDict", + cmd: "PublishCommand", ) -> "AnyDict": return { SpanAttributes.MESSAGING_SYSTEM: self.messaging_system, - SpanAttributes.MESSAGING_DESTINATION_NAME: self._get_destination(kwargs), - SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: kwargs["correlation_id"], + SpanAttributes.MESSAGING_DESTINATION_NAME: cmd.destination, + SpanAttributes.MESSAGING_MESSAGE_CONVERSATION_ID: cmd.correlation_id, } def get_publish_destination_name( self, - kwargs: "AnyDict", + cmd: "PublishCommand", ) -> str: - return self._get_destination(kwargs) + return cmd.destination @staticmethod def _get_destination(kwargs: "AnyDict") -> str: diff --git a/faststream/redis/parser.py b/faststream/redis/parser.py index d42297af77..1e823b2349 100644 --- a/faststream/redis/parser.py +++ b/faststream/redis/parser.py @@ -1,23 +1,20 @@ +from collections.abc import Mapping, Sequence from typing import ( TYPE_CHECKING, Any, - List, - Mapping, Optional, - Sequence, - Tuple, - Type, TypeVar, Union, ) -from faststream._compat import dump_json, json_loads -from faststream.broker.message import ( +from faststream._internal._compat import dump_json, json_dumps, json_loads +from faststream._internal.basic_types import AnyDict, DecodedMessage, SendableMessage +from faststream._internal.constants import ContentTypes +from faststream.message import ( decode_message, encode_message, gen_cor_id, ) -from faststream.constants import ContentTypes from faststream.redis.message import ( RedisBatchListMessage, RedisBatchStreamMessage, @@ -26,12 +23,13 @@ RedisStreamMessage, bDATA_KEY, ) -from faststream.types import AnyDict, DecodedMessage, SendableMessage if TYPE_CHECKING: from re import Pattern - from faststream.broker.message import StreamMessage + from fast_depends.library.serializer import SerializerProto + + from faststream.message import StreamMessage MsgType = TypeVar("MsgType", bound=Mapping[str, Any]) @@ -58,11 +56,12 @@ def build( cls, *, message: Union[Sequence["SendableMessage"], "SendableMessage"], - reply_to: Optional[str], + reply_to: str | None, headers: Optional["AnyDict"], correlation_id: str, + serializer: Optional["SerializerProto"] = None ) -> "RawMessage": - payload, content_type = encode_message(message) + payload, content_type = encode_message(message, serializer=serializer) headers_to_send = { "correlation_id": correlation_id, @@ -87,44 +86,44 @@ def encode( cls, *, message: Union[Sequence["SendableMessage"], "SendableMessage"], - reply_to: Optional[str], + reply_to: str | None, headers: Optional["AnyDict"], correlation_id: str, + serializer: Optional["SerializerProto"] = None ) -> bytes: msg = cls.build( message=message, reply_to=reply_to, headers=headers, correlation_id=correlation_id, + serializer=serializer ) - return dump_json( - { - "data": msg.data, - "headers": msg.headers, - } - ) + return json_dumps({ + "data": msg.data.decode(), + "headers": msg.headers, + }) @staticmethod - def parse(data: bytes) -> Tuple[bytes, "AnyDict"]: + def parse(data: bytes) -> tuple[bytes, "AnyDict"]: headers: AnyDict try: # FastStream message format parsed_data = json_loads(data) - data = parsed_data["data"].encode() + final_data = parsed_data["data"].encode() headers = parsed_data["headers"] except Exception: # Raw Redis message format - data = data + final_data = data headers = {} - return data, headers + return final_data, headers class SimpleParser: - msg_class: Type["StreamMessage[Any]"] + msg_class: type["StreamMessage[Any]"] def __init__( self, @@ -155,19 +154,18 @@ async def parse_message( def _parse_data( self, message: Mapping[str, Any], - ) -> Tuple[bytes, "AnyDict", List["AnyDict"]]: + ) -> tuple[bytes, "AnyDict", list["AnyDict"]]: return (*RawMessage.parse(message["data"]), []) def get_path(self, message: Mapping[str, Any]) -> "AnyDict": if ( - message.get("pattern") - and (path_re := self.pattern) + (path_re := self.pattern) + and message.get("pattern") and (match := path_re.match(message["channel"])) ): return match.groupdict() - else: - return {} + return {} async def decode_message( self, @@ -190,9 +188,9 @@ class RedisBatchListParser(SimpleParser): def _parse_data( self, message: Mapping[str, Any], - ) -> Tuple[bytes, "AnyDict", List["AnyDict"]]: - body: List[Any] = [] - batch_headers: List[AnyDict] = [] + ) -> tuple[bytes, "AnyDict", list["AnyDict"]]: + body: list[Any] = [] + batch_headers: list[AnyDict] = [] for x in message["data"]: msg_data, msg_headers = _decode_batch_body_item(x) @@ -205,7 +203,7 @@ def _parse_data( dump_json(body), { **first_msg_headers, - "content-type": ContentTypes.json.value, + "content-type": ContentTypes.JSON.value, }, batch_headers, ) @@ -216,8 +214,9 @@ class RedisStreamParser(SimpleParser): @classmethod def _parse_data( - cls, message: Mapping[str, Any] - ) -> Tuple[bytes, "AnyDict", List["AnyDict"]]: + cls, + message: Mapping[str, Any], + ) -> tuple[bytes, "AnyDict", list["AnyDict"]]: data = message["data"] return (*RawMessage.parse(data.get(bDATA_KEY) or dump_json(data)), []) @@ -228,9 +227,9 @@ class RedisBatchStreamParser(SimpleParser): def _parse_data( self, message: Mapping[str, Any], - ) -> Tuple[bytes, "AnyDict", List["AnyDict"]]: - body: List[Any] = [] - batch_headers: List[AnyDict] = [] + ) -> tuple[bytes, "AnyDict", list["AnyDict"]]: + body: list[Any] = [] + batch_headers: list[AnyDict] = [] for x in message["data"]: msg_data, msg_headers = _decode_batch_body_item(x.get(bDATA_KEY, x)) @@ -243,13 +242,13 @@ def _parse_data( dump_json(body), { **first_msg_headers, - "content-type": ContentTypes.json.value, + "content-type": ContentTypes.JSON.value, }, batch_headers, ) -def _decode_batch_body_item(msg_content: bytes) -> Tuple[Any, "AnyDict"]: +def _decode_batch_body_item(msg_content: bytes) -> tuple[Any, "AnyDict"]: msg_body, headers = RawMessage.parse(msg_content) try: return json_loads(msg_body), headers diff --git a/faststream/redis/prometheus/middleware.py b/faststream/redis/prometheus/middleware.py index 1b157cb5a9..0d343ee8fb 100644 --- a/faststream/redis/prometheus/middleware.py +++ b/faststream/redis/prometheus/middleware.py @@ -1,21 +1,24 @@ -from typing import TYPE_CHECKING, Optional, Sequence +from collections.abc import Sequence +from typing import TYPE_CHECKING -from faststream.prometheus.middleware import BasePrometheusMiddleware +from faststream._internal.basic_types import AnyDict +from faststream._internal.constants import EMPTY +from faststream.prometheus.middleware import PrometheusMiddleware from faststream.redis.prometheus.provider import settings_provider_factory -from faststream.types import EMPTY +from faststream.redis.response import RedisPublishCommand if TYPE_CHECKING: from prometheus_client import CollectorRegistry -class RedisPrometheusMiddleware(BasePrometheusMiddleware): +class RedisPrometheusMiddleware(PrometheusMiddleware[RedisPublishCommand, AnyDict]): def __init__( self, *, registry: "CollectorRegistry", app_name: str = EMPTY, metrics_prefix: str = "faststream", - received_messages_size_buckets: Optional[Sequence[float]] = None, + received_messages_size_buckets: Sequence[float] | None = None, ) -> None: super().__init__( settings_provider_factory=settings_provider_factory, diff --git a/faststream/redis/prometheus/provider.py b/faststream/redis/prometheus/provider.py index b93120ff4c..ff95656996 100644 --- a/faststream/redis/prometheus/provider.py +++ b/faststream/redis/prometheus/provider.py @@ -1,30 +1,29 @@ -from typing import TYPE_CHECKING, Optional, Sized, Union, cast +from typing import TYPE_CHECKING, Optional from faststream.prometheus import ( ConsumeAttrs, MetricsSettingsProvider, ) +from faststream.redis.response import RedisPublishCommand if TYPE_CHECKING: - from faststream.broker.message import StreamMessage - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict + from faststream.message.message import StreamMessage -class BaseRedisMetricsSettingsProvider(MetricsSettingsProvider["AnyDict"]): +class BaseRedisMetricsSettingsProvider( + MetricsSettingsProvider["AnyDict", RedisPublishCommand] +): __slots__ = ("messaging_system",) def __init__(self) -> None: self.messaging_system = "redis" - def get_publish_destination_name_from_kwargs( + def get_publish_destination_name_from_cmd( self, - kwargs: "AnyDict", + cmd: RedisPublishCommand, ) -> str: - return self._get_destination(kwargs) - - @staticmethod - def _get_destination(kwargs: "AnyDict") -> str: - return kwargs.get("channel") or kwargs.get("list") or kwargs.get("stream") or "" + return cmd.destination class RedisMetricsSettingsProvider(BaseRedisMetricsSettingsProvider): @@ -33,7 +32,7 @@ def get_consume_attrs_from_message( msg: "StreamMessage[AnyDict]", ) -> ConsumeAttrs: return { - "destination_name": self._get_destination(msg.raw_message), + "destination_name": _get_destination(msg.raw_message), "message_size": len(msg.body), "messages_count": 1, } @@ -45,19 +44,19 @@ def get_consume_attrs_from_message( msg: "StreamMessage[AnyDict]", ) -> ConsumeAttrs: return { - "destination_name": self._get_destination(msg.raw_message), + "destination_name": _get_destination(msg.raw_message), "message_size": len(msg.body), - "messages_count": len(cast("Sized", msg._decoded_body)), + "messages_count": len(msg.raw_message["data"]), } def settings_provider_factory( msg: Optional["AnyDict"], -) -> Union[ - RedisMetricsSettingsProvider, - BatchRedisMetricsSettingsProvider, -]: +) -> RedisMetricsSettingsProvider | BatchRedisMetricsSettingsProvider: if msg is not None and msg.get("type", "").startswith("b"): return BatchRedisMetricsSettingsProvider() - else: - return RedisMetricsSettingsProvider() + return RedisMetricsSettingsProvider() + + +def _get_destination(kwargs: "AnyDict") -> str: + return kwargs.get("channel") or kwargs.get("list") or kwargs.get("stream") or "" diff --git a/faststream/redis/publisher/asyncapi.py b/faststream/redis/publisher/asyncapi.py deleted file mode 100644 index fe1d4d7a90..0000000000 --- a/faststream/redis/publisher/asyncapi.py +++ /dev/null @@ -1,188 +0,0 @@ -from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union - -from typing_extensions import TypeAlias, override - -from faststream.asyncapi.schema import ( - Channel, - ChannelBinding, - CorrelationId, - Message, - Operation, -) -from faststream.asyncapi.schema.bindings import redis -from faststream.asyncapi.utils import resolve_payloads -from faststream.exceptions import SetupError -from faststream.redis.publisher.usecase import ( - ChannelPublisher, - ListBatchPublisher, - ListPublisher, - LogicPublisher, - StreamPublisher, -) -from faststream.redis.schemas import INCORRECT_SETUP_MSG, ListSub, PubSub, StreamSub -from faststream.redis.schemas.proto import RedisAsyncAPIProtocol, validate_options - -if TYPE_CHECKING: - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware - from faststream.redis.message import UnifyRedisDict - from faststream.types import AnyDict - -PublisherType: TypeAlias = Union[ - "AsyncAPIChannelPublisher", - "AsyncAPIStreamPublisher", - "AsyncAPIListPublisher", - "AsyncAPIListBatchPublisher", -] - - -class AsyncAPIPublisher(LogicPublisher, RedisAsyncAPIProtocol): - """A class to represent a Redis publisher.""" - - def get_schema(self) -> Dict[str, Channel]: - payloads = self.get_payloads() - - return { - self.name: Channel( - description=self.description, - publish=Operation( - message=Message( - title=f"{self.name}:Message", - payload=resolve_payloads(payloads, "Publisher"), - correlationId=CorrelationId( - location="$message.header#/correlation_id" - ), - ), - ), - bindings=ChannelBinding( - redis=self.channel_binding, - ), - ) - } - - @override - @staticmethod - def create( # type: ignore[override] - *, - channel: Union["PubSub", str, None], - list: Union["ListSub", str, None], - stream: Union["StreamSub", str, None], - headers: Optional["AnyDict"], - reply_to: str, - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - schema_: Optional[Any], - include_in_schema: bool, - ) -> PublisherType: - validate_options(channel=channel, list=list, stream=stream) - - if (channel := PubSub.validate(channel)) is not None: - return AsyncAPIChannelPublisher( - channel=channel, - # basic args - headers=headers, - reply_to=reply_to, - broker_middlewares=broker_middlewares, - middlewares=middlewares, - # AsyncAPI args - title_=title_, - description_=description_, - schema_=schema_, - include_in_schema=include_in_schema, - ) - - elif (stream := StreamSub.validate(stream)) is not None: - return AsyncAPIStreamPublisher( - stream=stream, - # basic args - headers=headers, - reply_to=reply_to, - broker_middlewares=broker_middlewares, - middlewares=middlewares, - # AsyncAPI args - title_=title_, - description_=description_, - schema_=schema_, - include_in_schema=include_in_schema, - ) - - elif (list := ListSub.validate(list)) is not None: - if list.batch: - return AsyncAPIListBatchPublisher( - list=list, - # basic args - headers=headers, - reply_to=reply_to, - broker_middlewares=broker_middlewares, - middlewares=middlewares, - # AsyncAPI args - title_=title_, - description_=description_, - schema_=schema_, - include_in_schema=include_in_schema, - ) - else: - return AsyncAPIListPublisher( - list=list, - # basic args - headers=headers, - reply_to=reply_to, - broker_middlewares=broker_middlewares, - middlewares=middlewares, - # AsyncAPI args - title_=title_, - description_=description_, - schema_=schema_, - include_in_schema=include_in_schema, - ) - - else: - raise SetupError(INCORRECT_SETUP_MSG) - - -class AsyncAPIChannelPublisher(ChannelPublisher, AsyncAPIPublisher): - def get_name(self) -> str: - return f"{self.channel.name}:Publisher" - - @property - def channel_binding(self) -> "redis.ChannelBinding": - return redis.ChannelBinding( - channel=self.channel.name, - method="publish", - ) - - -class _ListPublisherMixin(AsyncAPIPublisher): - list: "ListSub" - - def get_name(self) -> str: - return f"{self.list.name}:Publisher" - - @property - def channel_binding(self) -> "redis.ChannelBinding": - return redis.ChannelBinding( - channel=self.list.name, - method="rpush", - ) - - -class AsyncAPIListPublisher(ListPublisher, _ListPublisherMixin): - pass - - -class AsyncAPIListBatchPublisher(ListBatchPublisher, _ListPublisherMixin): - pass - - -class AsyncAPIStreamPublisher(StreamPublisher, AsyncAPIPublisher): - def get_name(self) -> str: - return f"{self.stream.name}:Publisher" - - @property - def channel_binding(self) -> "redis.ChannelBinding": - return redis.ChannelBinding( - channel=self.stream.name, - method="xadd", - ) diff --git a/faststream/redis/publisher/config.py b/faststream/redis/publisher/config.py new file mode 100644 index 0000000000..8e88b17ed1 --- /dev/null +++ b/faststream/redis/publisher/config.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass +from typing import Any + +from faststream._internal.configs import ( + PublisherSpecificationConfig, + PublisherUsecaseConfig, +) +from faststream.redis.configs import RedisBrokerConfig + + +class RedisPublisherSpecificationConfig(PublisherSpecificationConfig): + pass + + +@dataclass(kw_only=True) +class RedisPublisherConfig(PublisherUsecaseConfig): + _outer_config: RedisBrokerConfig + + reply_to: str + headers: dict[str, Any] | None diff --git a/faststream/redis/publisher/factory.py b/faststream/redis/publisher/factory.py new file mode 100644 index 0000000000..0b36505ad1 --- /dev/null +++ b/faststream/redis/publisher/factory.py @@ -0,0 +1,87 @@ +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Optional, TypeAlias, Union + +from faststream.exceptions import SetupError +from faststream.redis.schemas import INCORRECT_SETUP_MSG, ListSub, PubSub, StreamSub +from faststream.redis.schemas.proto import validate_options + +from .config import RedisPublisherConfig, RedisPublisherSpecificationConfig +from .specification import ( + ChannelPublisherSpecification, + ListPublisherSpecification, + RedisPublisherSpecification, + StreamPublisherSpecification, +) +from .usecase import ( + ChannelPublisher, + ListBatchPublisher, + ListPublisher, + StreamPublisher, +) + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict + from faststream._internal.types import PublisherMiddleware + from faststream.redis.configs import RedisBrokerConfig + + +PublisherType: TypeAlias = ( + ChannelPublisher | StreamPublisher | ListPublisher | ListBatchPublisher +) + + +def create_publisher( + *, + channel: Union["PubSub", str, None], + list: Union["ListSub", str, None], + stream: Union["StreamSub", str, None], + headers: Optional["AnyDict"], + reply_to: str, + config: "RedisBrokerConfig", + middlewares: Sequence["PublisherMiddleware"], + # AsyncAPI args + title_: str | None, + description_: str | None, + schema_: Any | None, + include_in_schema: bool, +) -> PublisherType: + validate_options(channel=channel, list=list, stream=stream) + + publisher_config = RedisPublisherConfig( + reply_to=reply_to, + headers=headers, + middlewares=middlewares, + _outer_config=config, + ) + + specification_config = RedisPublisherSpecificationConfig( + schema_=schema_, + title_=title_, + description_=description_, + include_in_schema=include_in_schema, + ) + + specification: RedisPublisherSpecification + if (channel := PubSub.validate(channel)) is not None: + specification = ChannelPublisherSpecification( + config, specification_config, channel + ) + + return ChannelPublisher(publisher_config, specification, channel=channel) + + if (stream := StreamSub.validate(stream)) is not None: + specification = StreamPublisherSpecification( + config, specification_config, stream + ) + + return StreamPublisher(publisher_config, specification, stream=stream) + + if (list := ListSub.validate(list)) is not None: + specification = ListPublisherSpecification(config, specification_config, list) + + if list.batch: + return ListBatchPublisher(publisher_config, specification, list=list) + + return ListPublisher(publisher_config, specification, list=list) + + raise SetupError(INCORRECT_SETUP_MSG) diff --git a/faststream/redis/publisher/fake.py b/faststream/redis/publisher/fake.py new file mode 100644 index 0000000000..b91aad92c1 --- /dev/null +++ b/faststream/redis/publisher/fake.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING, Union + +from faststream._internal.endpoint.publisher.fake import FakePublisher +from faststream.redis.response import RedisPublishCommand + +if TYPE_CHECKING: + from faststream._internal.producer import ProducerProto + from faststream.response.response import PublishCommand + + +class RedisFakePublisher(FakePublisher): + """Publisher Interface implementation to use as RPC or REPLY TO answer publisher.""" + + def __init__( + self, + producer: "ProducerProto", + channel: str, + ) -> None: + super().__init__(producer=producer) + self.channel = channel + + def patch_command( + self, cmd: Union["PublishCommand", "RedisPublishCommand"] + ) -> "RedisPublishCommand": + cmd = super().patch_command(cmd) + real_cmd = RedisPublishCommand.from_cmd(cmd) + real_cmd.destination = self.channel + return real_cmd diff --git a/faststream/redis/publisher/producer.py b/faststream/redis/publisher/producer.py index 914b060379..db295adc09 100644 --- a/faststream/redis/publisher/producer.py +++ b/faststream/redis/publisher/producer.py @@ -1,41 +1,36 @@ -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Optional, cast import anyio from typing_extensions import override -from faststream.broker.publisher.proto import ProducerProto -from faststream.broker.utils import resolve_custom_func -from faststream.exceptions import WRONG_PUBLISH_ARGS, SetupError +from faststream._internal.endpoint.utils import resolve_custom_func +from faststream._internal.producer import ProducerProto +from faststream._internal.utils.nuid import NUID from faststream.redis.message import DATA_KEY from faststream.redis.parser import RawMessage, RedisPubSubParser -from faststream.redis.schemas import INCORRECT_SETUP_MSG -from faststream.utils.functions import timeout_scope -from faststream.utils.nuid import NUID +from faststream.redis.response import DestinationType, RedisPublishCommand if TYPE_CHECKING: - from redis.asyncio.client import Pipeline, PubSub, Redis + from fast_depends.library.serializer import SerializerProto - from faststream.broker.types import ( - AsyncCallable, - CustomCallable, - ) - from faststream.types import AnyDict, SendableMessage + from faststream._internal.types import AsyncCallable, CustomCallable + from faststream.redis.configs import ConnectionState class RedisFastProducer(ProducerProto): """A class to represent a Redis producer.""" - _connection: "Redis[bytes]" _decoder: "AsyncCallable" _parser: "AsyncCallable" def __init__( self, - connection: "Redis[bytes]", + connection: "ConnectionState", parser: Optional["CustomCallable"], decoder: Optional["CustomCallable"], ) -> None: self._connection = connection + self.serializer: SerializerProto | None = None default = RedisPubSubParser() self._parser = resolve_custom_func( @@ -50,141 +45,49 @@ def __init__( @override async def publish( # type: ignore[override] self, - message: "SendableMessage", - *, - correlation_id: str, - channel: Optional[str] = None, - list: Optional[str] = None, - stream: Optional[str] = None, - maxlen: Optional[int] = None, - headers: Optional["AnyDict"] = None, - reply_to: str = "", - rpc: bool = False, - rpc_timeout: Optional[float] = 30.0, - raise_timeout: bool = False, - pipeline: Optional["Pipeline[bytes]"] = None, - ) -> Optional[Any]: - if not any((channel, list, stream)): - raise SetupError(INCORRECT_SETUP_MSG) - - if pipeline is not None and rpc is True: - raise RuntimeError( - "You cannot use both rpc and pipeline arguments at the same time: " - "select only one delivery mechanism." - ) - - psub: Optional[PubSub] = None - if rpc: - if reply_to: - raise WRONG_PUBLISH_ARGS - nuid = NUID() - rpc_nuid = str(nuid.next(), "utf-8") - reply_to = rpc_nuid - psub = self._connection.pubsub() - await psub.subscribe(reply_to) - + cmd: "RedisPublishCommand", + ) -> int | bytes: msg = RawMessage.encode( - message=message, - reply_to=reply_to, - headers=headers, - correlation_id=correlation_id, + message=cmd.body, + reply_to=cmd.reply_to, + headers=cmd.headers, + correlation_id=cmd.correlation_id or "", + serializer=self.serializer ) - conn = pipeline or self._connection - if channel is not None: - await conn.publish(channel, msg) - elif list is not None: - await conn.rpush(list, msg) - elif stream is not None: - await conn.xadd( - name=stream, - fields={DATA_KEY: msg}, - maxlen=maxlen, - ) - else: - raise AssertionError("unreachable") - - if psub is None: - return None - - else: - m = None - with timeout_scope(rpc_timeout, raise_timeout): - # skip subscribe message - await psub.get_message( - ignore_subscribe_messages=True, - timeout=rpc_timeout or 0.0, - ) - - # get real response - m = await psub.get_message( - ignore_subscribe_messages=True, - timeout=rpc_timeout or 0.0, - ) - - await psub.unsubscribe() - await psub.aclose() # type: ignore[attr-defined] - - if m is None: - if raise_timeout: - raise TimeoutError() - else: - return None - else: - return await self._decoder(await self._parser(m)) + return await self.__publish(msg, cmd) @override async def request( # type: ignore[override] self, - message: "SendableMessage", - *, - correlation_id: str, - channel: Optional[str] = None, - list: Optional[str] = None, - stream: Optional[str] = None, - maxlen: Optional[int] = None, - headers: Optional["AnyDict"] = None, - timeout: Optional[float] = 30.0, + cmd: "RedisPublishCommand", ) -> "Any": - if not any((channel, list, stream)): - raise SetupError(INCORRECT_SETUP_MSG) - nuid = NUID() reply_to = str(nuid.next(), "utf-8") - psub = self._connection.pubsub() + psub = self._connection.client.pubsub() await psub.subscribe(reply_to) msg = RawMessage.encode( - message=message, + message=cmd.body, reply_to=reply_to, - headers=headers, - correlation_id=correlation_id, + headers=cmd.headers, + correlation_id=cmd.correlation_id or "", + serializer=self.serializer ) - if channel is not None: - await self._connection.publish(channel, msg) - elif list is not None: - await self._connection.rpush(list, msg) - elif stream is not None: - await self._connection.xadd( - name=stream, - fields={DATA_KEY: msg}, - maxlen=maxlen, - ) - else: - raise AssertionError("unreachable") + await self.__publish(msg, cmd) - with anyio.fail_after(timeout) as scope: + with anyio.fail_after(cmd.timeout) as scope: # skip subscribe message await psub.get_message( ignore_subscribe_messages=True, - timeout=timeout or 0.0, + timeout=cmd.timeout or 0.0, ) # get real response response_msg = await psub.get_message( ignore_subscribe_messages=True, - timeout=timeout or 0.0, + timeout=cmd.timeout or 0.0, ) await psub.unsubscribe() @@ -195,22 +98,50 @@ async def request( # type: ignore[override] return response_msg + @override async def publish_batch( self, - *msgs: "SendableMessage", - list: str, - correlation_id: str, - headers: Optional["AnyDict"] = None, - pipeline: Optional["Pipeline[bytes]"] = None, - ) -> None: - batch = ( + cmd: "RedisPublishCommand", + ) -> int: + batch = [ RawMessage.encode( message=msg, - correlation_id=correlation_id, - reply_to=None, - headers=headers, + correlation_id=cmd.correlation_id or "", + reply_to=cmd.reply_to, + headers=cmd.headers, + serializer=self.serializer ) - for msg in msgs - ) - conn = pipeline or self._connection - await conn.rpush(list, *batch) + for msg in cmd.batch_bodies + ] + + connection = cmd.pipeline or self._connection.client + return await connection.rpush(cmd.destination, *batch) + + async def __publish( + self, + msg: bytes, + cmd: "RedisPublishCommand", + ) -> int | bytes: + connection = cmd.pipeline or self._connection.client + + if cmd.destination_type is DestinationType.Channel: + return await connection.publish(cmd.destination, msg) + + if cmd.destination_type is DestinationType.List: + return await connection.rpush(cmd.destination, msg) + + if cmd.destination_type is DestinationType.Stream: + return cast( + "bytes", + await connection.xadd( + name=cmd.destination, + fields={DATA_KEY: msg}, + maxlen=cmd.maxlen, + ), + ) + + error_msg = "unreachable" + raise AssertionError(error_msg) + + def connect(self, serializer: Optional["SerializerProto"] = None) -> None: + self.serializer = serializer diff --git a/faststream/redis/publisher/specification.py b/faststream/redis/publisher/specification.py new file mode 100644 index 0000000000..ec3ff6fe98 --- /dev/null +++ b/faststream/redis/publisher/specification.py @@ -0,0 +1,122 @@ +from faststream._internal.endpoint.publisher import PublisherSpecification +from faststream.redis.configs import RedisBrokerConfig +from faststream.redis.schemas import ListSub, PubSub, StreamSub +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 .config import RedisPublisherSpecificationConfig + + +class RedisPublisherSpecification( + PublisherSpecification[RedisBrokerConfig, RedisPublisherSpecificationConfig] +): + def get_schema(self) -> dict[str, PublisherSpec]: + payloads = self.get_payloads() + + return { + self.name: PublisherSpec( + description=self.config.description_, + operation=Operation( + message=Message( + title=f"{self.name}:Message", + payload=resolve_payloads(payloads, "Publisher"), + ), + bindings=None, + ), + bindings=ChannelBinding( + redis=self.channel_binding, + ), + ), + } + + @property + def channel_binding(self) -> redis.ChannelBinding: + raise NotImplementedError + + +class ChannelPublisherSpecification(RedisPublisherSpecification): + def __init__( + self, + _outer_config: RedisBrokerConfig, + specification_config: RedisPublisherSpecificationConfig, + channel: PubSub, + ) -> None: + super().__init__(_outer_config, specification_config) + self.channel = channel + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{self.channel_name}:Publisher" + + @property + def channel_name(self) -> str: + return f"{self._outer_config.prefix}{self.channel.name}" + + @property + def channel_binding(self) -> redis.ChannelBinding: + return redis.ChannelBinding( + channel=self.channel_name, + method="publish", + ) + + +class ListPublisherSpecification(RedisPublisherSpecification): + def __init__( + self, + _outer_config: RedisBrokerConfig, + specification_config: RedisPublisherSpecificationConfig, + list_sub: ListSub, + ) -> None: + super().__init__(_outer_config, specification_config) + self.list_sub = list_sub + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{self.list_name}:Publisher" + + @property + def list_name(self) -> str: + return f"{self._outer_config.prefix}{self.list_sub.name}" + + @property + def channel_binding(self) -> redis.ChannelBinding: + return redis.ChannelBinding( + channel=self.list_name, + method="rpush", + ) + + +class StreamPublisherSpecification(RedisPublisherSpecification): + def __init__( + self, + _outer_config: RedisBrokerConfig, + specification_config: RedisPublisherSpecificationConfig, + stream_sub: StreamSub, + ) -> None: + super().__init__(_outer_config, specification_config) + self.stream_sub = stream_sub + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{self.stream_name}:Publisher" + + @property + def stream_name(self) -> str: + return f"{self._outer_config.prefix}{self.stream_sub.name}" + + @property + def channel_binding(self) -> "redis.ChannelBinding": + return redis.ChannelBinding( + channel=self.stream_name, + method="xadd", + ) diff --git a/faststream/redis/publisher/usecase.py b/faststream/redis/publisher/usecase.py index aa93aa4a07..0ea55da9d7 100644 --- a/faststream/redis/publisher/usecase.py +++ b/faststream/redis/publisher/usecase.py @@ -1,98 +1,69 @@ from abc import abstractmethod -from contextlib import AsyncExitStack -from copy import deepcopy -from functools import partial -from itertools import chain -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable, Optional, Sequence +from collections.abc import Iterable +from typing import TYPE_CHECKING, Optional, Union -from typing_extensions import Annotated, Doc, deprecated, override +from typing_extensions import override -from faststream.broker.message import SourceType, gen_cor_id -from faststream.broker.publisher.usecase import PublisherUsecase -from faststream.exceptions import NOT_CONNECTED_YET +from faststream._internal.endpoint.publisher import PublisherUsecase +from faststream.message import gen_cor_id from faststream.redis.message import UnifyRedisDict -from faststream.redis.schemas import ListSub, PubSub, StreamSub -from faststream.utils.functions import return_input +from faststream.redis.response import RedisPublishCommand +from faststream.response.publish_type import PublishType if TYPE_CHECKING: from redis.asyncio.client import Pipeline - from faststream.broker.types import BrokerMiddleware, PublisherMiddleware + from faststream._internal.basic_types import AnyDict, SendableMessage + from faststream._internal.types import PublisherMiddleware from faststream.redis.message import RedisMessage - from faststream.redis.publisher.producer import RedisFastProducer - from faststream.types import AnyDict, AsyncFunc, SendableMessage + from faststream.redis.schemas import ListSub, PubSub, StreamSub + from faststream.response import PublishCommand + + from .config import RedisPublisherConfig + from .producer import RedisFastProducer + from .specification import ( + ChannelPublisherSpecification, + ListPublisherSpecification, + RedisPublisherSpecification, + StreamPublisherSpecification, + ) class LogicPublisher(PublisherUsecase[UnifyRedisDict]): """A class to represent a Redis publisher.""" - _producer: Optional["RedisFastProducer"] + _producer: "RedisFastProducer" def __init__( self, - *, - reply_to: str, - headers: Optional["AnyDict"], - # Publisher args - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - middlewares: Sequence["PublisherMiddleware"], - # AsyncAPI args - schema_: Optional[Any], - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, + config: "RedisPublisherConfig", + specification: "RedisPublisherSpecification", ) -> 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 - self.headers = headers + super().__init__(config, specification) - self._producer = None + self.reply_to = config.reply_to + self.headers = config.headers or {} @abstractmethod def subscriber_property(self, *, name_only: bool) -> "AnyDict": - raise NotImplementedError() + raise NotImplementedError class ChannelPublisher(LogicPublisher): def __init__( self, + config: "RedisPublisherConfig", + specification: "ChannelPublisherSpecification", *, channel: "PubSub", - reply_to: str, - headers: Optional["AnyDict"], - # Regular publisher options - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - middlewares: Sequence["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, - ) + super().__init__(config, specification) - self.channel = channel + self._channel = channel - def __hash__(self) -> int: - return hash(f"publisher:pubsub:{self.channel.name}") + @property + def channel(self) -> "PubSub": + return self._channel.add_prefix(self._outer_config.prefix) @override def subscriber_property(self, *, name_only: bool) -> "AnyDict": @@ -102,219 +73,83 @@ def subscriber_property(self, *, name_only: bool) -> "AnyDict": "stream": None, } - def add_prefix(self, prefix: str) -> None: - channel = deepcopy(self.channel) - channel.name = "".join((prefix, channel.name)) - self.channel = channel - @override async def publish( self, - message: Annotated[ - "SendableMessage", - Doc("Message body to send."), - ] = None, - channel: Annotated[ - Optional[str], - Doc("Redis PubSub object name to send message."), - ] = None, - reply_to: Annotated[ - str, - Doc("Reply message destination PubSub object name."), - ] = "", - headers: Annotated[ - Optional["AnyDict"], - Doc("Message headers to store metainformation."), - ] = None, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, + message: "SendableMessage" = None, + channel: str | None = None, + reply_to: str = "", + headers: Optional["AnyDict"] = None, + correlation_id: str | None = None, *, - # rpc args - rpc: Annotated[ - bool, - Doc("Whether to wait for reply in blocking mode."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - rpc_timeout: Annotated[ - Optional[float], - Doc("RPC reply waiting time."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method with `timeout` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = 30.0, - raise_timeout: Annotated[ - bool, - Doc( - "Whetever to raise `TimeoutError` or return `None` at **rpc_timeout**. " - "RPC request returns `None` at timeout by default." - ), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "`request` always raises TimeoutError instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - pipeline: Annotated[ - Optional["Pipeline[bytes]"], - Doc( - "Optional Redis `Pipeline` object to batch multiple commands. " - "Use it to group Redis operations for optimized execution and reduced latency." - ), - ] = None, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), - **kwargs: Any, # option to suppress maxlen - ) -> Optional[Any]: - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - channel_sub = PubSub.validate(channel or self.channel) - reply_to = reply_to or self.reply_to - headers = headers or self.headers - correlation_id = correlation_id or gen_cor_id() - - call: AsyncFunc = self._producer.publish - - for m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - call = partial(m, call) - - return await call( + pipeline: Optional["Pipeline[bytes]"] = None, + ) -> int: + cmd = RedisPublishCommand( message, - channel=channel_sub.name, + channel=channel or self.channel.name, + reply_to=reply_to or self.reply_to, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.PUBLISH, pipeline=pipeline, - # basic args - reply_to=reply_to, - headers=headers, - correlation_id=correlation_id, - # RPC args - rpc=rpc, - rpc_timeout=rpc_timeout, - raise_timeout=raise_timeout, ) + return await self._basic_publish(cmd, _extra_middlewares=()) + + @override + async def _publish( + self, + cmd: Union["PublishCommand", "RedisPublishCommand"], + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> None: + """This method should be called in subscriber flow only.""" + cmd = RedisPublishCommand.from_cmd(cmd) + + cmd.set_destination(channel=self.channel.name) + + cmd.add_headers(self.headers, override=False) + cmd.reply_to = cmd.reply_to or self.reply_to + + await self._basic_publish(cmd, _extra_middlewares=_extra_middlewares) @override async def request( self, - message: Annotated[ - "SendableMessage", - Doc("Message body to send."), - ] = None, - channel: Annotated[ - Optional[str], - Doc("Redis PubSub object name to send message."), - ] = None, + message: "SendableMessage" = None, + channel: str | None = None, *, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - headers: Annotated[ - Optional["AnyDict"], - Doc("Message headers to store metainformation."), - ] = None, - timeout: Annotated[ - Optional[float], - Doc("RPC reply waiting time."), - ] = 30.0, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), + correlation_id: str | None = None, + headers: Optional["AnyDict"] = None, + timeout: float | None = 30.0, ) -> "RedisMessage": - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - kwargs = { - "channel": PubSub.validate(channel or self.channel).name, - # basic args - "headers": headers or self.headers, - "correlation_id": correlation_id or gen_cor_id(), - "timeout": timeout, - } - request: AsyncFunc = self._producer.request - - for pub_m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - request = partial(pub_m, request) - - published_msg = await request( + cmd = RedisPublishCommand( message, - **kwargs, + channel=channel or self.channel.name, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.REQUEST, + timeout=timeout, ) - async with AsyncExitStack() as stack: - return_msg: Callable[[RedisMessage], Awaitable[RedisMessage]] = return_input - for m in self._broker_middlewares[::-1]: - mid = m(published_msg) - await stack.enter_async_context(mid) - return_msg = partial(mid.consume_scope, return_msg) - - parsed_msg = await self._producer._parser(published_msg) - parsed_msg._decoded_body = await self._producer._decoder(parsed_msg) - parsed_msg._source_type = SourceType.Response - return await return_msg(parsed_msg) - - raise AssertionError("unreachable") + msg: RedisMessage = await self._basic_request(cmd) + return msg class ListPublisher(LogicPublisher): def __init__( self, + config: "RedisPublisherConfig", + specification: "ListPublisherSpecification", *, list: "ListSub", - reply_to: str, - headers: Optional["AnyDict"], - # Regular publisher options - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - middlewares: Sequence["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, - ) + super().__init__(config, specification) - self.list = list + self._list = list - def __hash__(self) -> int: - return hash(f"publisher:list:{self.list.name}") + @property + def list(self) -> "ListSub": + return self._list.add_prefix(self._outer_config.prefix) @override def subscriber_property(self, *, name_only: bool) -> "AnyDict": @@ -324,279 +159,124 @@ def subscriber_property(self, *, name_only: bool) -> "AnyDict": "stream": None, } - def add_prefix(self, prefix: str) -> None: - list_sub = deepcopy(self.list) - list_sub.name = "".join((prefix, list_sub.name)) - self.list = list_sub - @override async def publish( self, - message: Annotated[ - "SendableMessage", - Doc("Message body to send."), - ] = None, - list: Annotated[ - Optional[str], - Doc("Redis List object name to send message."), - ] = None, - reply_to: Annotated[ - str, - Doc("Reply message destination PubSub object name."), - ] = "", - headers: Annotated[ - Optional["AnyDict"], - Doc("Message headers to store metainformation."), - ] = None, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, + message: "SendableMessage" = None, + list: str | None = None, + reply_to: str = "", + headers: Optional["AnyDict"] = None, + correlation_id: str | None = None, *, - # rpc args - rpc: Annotated[ - bool, - Doc("Whether to wait for reply in blocking mode."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - rpc_timeout: Annotated[ - Optional[float], - Doc("RPC reply waiting time."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method with `timeout` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = 30.0, - raise_timeout: Annotated[ - bool, - Doc( - "Whetever to raise `TimeoutError` or return `None` at **rpc_timeout**. " - "RPC request returns `None` at timeout by default." - ), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "`request` always raises TimeoutError instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - pipeline: Annotated[ - Optional["Pipeline[bytes]"], - Doc( - "Optional Redis `Pipeline` object to batch multiple commands. " - "Use it to group Redis operations for optimized execution and reduced latency." - ), - ] = None, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), - **kwargs: Any, # option to suppress maxlen - ) -> Any: - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - list_sub = ListSub.validate(list or self.list) - reply_to = reply_to or self.reply_to - correlation_id = correlation_id or gen_cor_id() - - call: AsyncFunc = self._producer.publish - - for m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - call = partial(m, call) - - return await call( + pipeline: Optional["Pipeline[bytes]"] = None, + ) -> int: + cmd = RedisPublishCommand( message, - list=list_sub.name, + list=list or self.list.name, + reply_to=reply_to or self.reply_to, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.PUBLISH, pipeline=pipeline, - # basic args - reply_to=reply_to, - headers=headers or self.headers, - correlation_id=correlation_id, - # RPC args - rpc=rpc, - rpc_timeout=rpc_timeout, - raise_timeout=raise_timeout, ) + return await self._basic_publish(cmd, _extra_middlewares=()) + @override - async def request( + async def _publish( self, - message: Annotated[ - "SendableMessage", - Doc("Message body to send."), - ] = None, - list: Annotated[ - Optional[str], - Doc("Redis List object name to send message."), - ] = None, + cmd: Union["PublishCommand", "RedisPublishCommand"], *, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - headers: Annotated[ - Optional["AnyDict"], - Doc("Message headers to store metainformation."), - ] = None, - timeout: Annotated[ - Optional[float], - Doc("RPC reply waiting time."), - ] = 30.0, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), - ) -> "RedisMessage": - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - kwargs = { - "list": ListSub.validate(list or self.list).name, - # basic args - "headers": headers or self.headers, - "correlation_id": correlation_id or gen_cor_id(), - "timeout": timeout, - } + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> None: + """This method should be called in subscriber flow only.""" + cmd = RedisPublishCommand.from_cmd(cmd) + + cmd.set_destination(list=self.list.name) - request: AsyncFunc = self._producer.request + cmd.add_headers(self.headers, override=False) + cmd.reply_to = cmd.reply_to or self.reply_to - for pub_m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - request = partial(pub_m, request) + await self._basic_publish(cmd, _extra_middlewares=_extra_middlewares) - published_msg = await request( + @override + async def request( + self, + message: "SendableMessage" = None, + list: str | None = None, + *, + correlation_id: str | None = None, + headers: Optional["AnyDict"] = None, + timeout: float | None = 30.0, + ) -> "RedisMessage": + cmd = RedisPublishCommand( message, - **kwargs, + list=list or self.list.name, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.REQUEST, + timeout=timeout, ) - async with AsyncExitStack() as stack: - return_msg: Callable[[RedisMessage], Awaitable[RedisMessage]] = return_input - for m in self._broker_middlewares[::-1]: - mid = m(published_msg) - await stack.enter_async_context(mid) - return_msg = partial(mid.consume_scope, return_msg) - - parsed_msg = await self._producer._parser(published_msg) - parsed_msg._decoded_body = await self._producer._decoder(parsed_msg) - parsed_msg._source_type = SourceType.Response - return await return_msg(parsed_msg) - - raise AssertionError("unreachable") + msg: RedisMessage = await self._basic_request(cmd) + return msg class ListBatchPublisher(ListPublisher): @override async def publish( # type: ignore[override] self, - message: Annotated[ - Iterable["SendableMessage"], - Doc("Message body to send."), - ] = (), - list: Annotated[ - Optional[str], - Doc("Redis List object name to send message."), - ] = None, - *, - correlation_id: Annotated[ - Optional[str], - Doc("Has no real effect. Option to be compatible with original protocol."), - ] = None, - headers: Annotated[ - Optional["AnyDict"], - Doc("Message headers to store metainformation."), - ] = None, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), - pipeline: Annotated[ - Optional["Pipeline[bytes]"], - Doc( - "Optional Redis `Pipeline` object to batch multiple commands. " - "Use it to group Redis operations for optimized execution and reduced latency." - ), - ] = None, - **kwargs: Any, # option to suppress maxlen - ) -> None: - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - list_sub = ListSub.validate(list or self.list) - correlation_id = correlation_id or gen_cor_id() - - call: AsyncFunc = self._producer.publish_batch - - for m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - call = partial(m, call) - - await call( - *message, - list=list_sub.name, - correlation_id=correlation_id, - headers=headers or self.headers, + *messages: "SendableMessage", + list: str, + correlation_id: str | None = None, + reply_to: str = "", + headers: Optional["AnyDict"] = None, + pipeline: Optional["Pipeline[bytes]"] = None, + ) -> int: + cmd = RedisPublishCommand( + *messages, + list=list or self.list.name, + reply_to=reply_to or self.reply_to, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.PUBLISH, pipeline=pipeline, ) + return await self._basic_publish_batch(cmd, _extra_middlewares=()) + + @override + async def _publish( # type: ignore[override] + self, + cmd: Union["PublishCommand", "RedisPublishCommand"], + *, + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> None: + """This method should be called in subscriber flow only.""" + cmd = RedisPublishCommand.from_cmd(cmd, batch=True) + + cmd.set_destination(list=self.list.name) + + cmd.add_headers(self.headers, override=False) + cmd.reply_to = cmd.reply_to or self.reply_to + + await self._basic_publish_batch(cmd, _extra_middlewares=_extra_middlewares) + class StreamPublisher(LogicPublisher): def __init__( self, + config: "RedisPublisherConfig", + specification: "StreamPublisherSpecification", *, stream: "StreamSub", - reply_to: str, - headers: Optional["AnyDict"], - # Regular publisher options - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - middlewares: Sequence["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 + super().__init__(config, specification) + self._stream = stream - def __hash__(self) -> int: - return hash(f"publisher:stream:{self.stream.name}") + @property + def stream(self) -> "StreamSub": + return self._stream.add_prefix(self._outer_config.prefix) @override def subscriber_property(self, *, name_only: bool) -> "AnyDict": @@ -606,199 +286,69 @@ def subscriber_property(self, *, name_only: bool) -> "AnyDict": "stream": self.stream.name if name_only else self.stream, } - def add_prefix(self, prefix: str) -> None: - stream_sub = deepcopy(self.stream) - stream_sub.name = "".join((prefix, stream_sub.name)) - self.stream = stream_sub - @override async def publish( self, - message: Annotated[ - "SendableMessage", - Doc("Message body to send."), - ] = None, - stream: Annotated[ - Optional[str], - Doc("Redis Stream object name to send message."), - ] = None, - reply_to: Annotated[ - str, - Doc("Reply message destination PubSub object name."), - ] = "", - headers: Annotated[ - Optional["AnyDict"], - Doc("Message headers to store metainformation."), - ] = None, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, + message: "SendableMessage" = None, + stream: str | None = None, + reply_to: str = "", + headers: Optional["AnyDict"] = None, + correlation_id: str | None = None, *, - maxlen: Annotated[ - Optional[int], - Doc( - "Redis Stream maxlen publish option. " - "Remove eldest message if maxlen exceeded." - ), - ] = None, - # rpc args - rpc: Annotated[ - bool, - Doc("Whether to wait for reply in blocking mode."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - rpc_timeout: Annotated[ - Optional[float], - Doc("RPC reply waiting time."), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "Please, use `request` method with `timeout` instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = 30.0, - raise_timeout: Annotated[ - bool, - Doc( - "Whetever to raise `TimeoutError` or return `None` at **rpc_timeout**. " - "RPC request returns `None` at timeout by default." - ), - deprecated( - "Deprecated in **FastStream 0.5.17**. " - "`request` always raises TimeoutError instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = False, - pipeline: Annotated[ - Optional["Pipeline[bytes]"], - Doc( - "Optional Redis `Pipeline` object to batch multiple commands. " - "Use it to group Redis operations for optimized execution and reduced latency." - ), - ] = None, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), - ) -> Optional[Any]: - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - stream_sub = StreamSub.validate(stream or self.stream) - maxlen = maxlen or stream_sub.maxlen - reply_to = reply_to or self.reply_to - headers = headers or self.headers - correlation_id = correlation_id or gen_cor_id() - - call: AsyncFunc = self._producer.publish - - for m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - call = partial(m, call) - - return await call( + maxlen: int | None = None, + pipeline: Optional["Pipeline[bytes]"] = None, + ) -> bytes: + cmd = RedisPublishCommand( message, - stream=stream_sub.name, - maxlen=maxlen, + stream=stream or self.stream.name, + reply_to=reply_to or self.reply_to, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), + maxlen=maxlen or self.stream.maxlen, + _publish_type=PublishType.PUBLISH, pipeline=pipeline, - # basic args - reply_to=reply_to, - headers=headers, - correlation_id=correlation_id, - # RPC args - rpc=rpc, - rpc_timeout=rpc_timeout, - raise_timeout=raise_timeout, ) + return await self._basic_publish(cmd, _extra_middlewares=()) + @override - async def request( + async def _publish( self, - message: Annotated[ - "SendableMessage", - Doc("Message body to send."), - ] = None, - stream: Annotated[ - Optional[str], - Doc("Redis Stream object name to send message."), - ] = None, + cmd: Union["PublishCommand", "RedisPublishCommand"], *, - maxlen: Annotated[ - Optional[int], - Doc( - "Redis Stream maxlen publish option. " - "Remove eldest message if maxlen exceeded." - ), - ] = None, - correlation_id: Annotated[ - Optional[str], - Doc( - "Manual message **correlation_id** setter. " - "**correlation_id** is a useful option to trace messages." - ), - ] = None, - headers: Annotated[ - Optional["AnyDict"], - Doc("Message headers to store metainformation."), - ] = None, - timeout: Annotated[ - Optional[float], - Doc("RPC reply waiting time."), - ] = 30.0, - # publisher specific - _extra_middlewares: Annotated[ - Iterable["PublisherMiddleware"], - Doc("Extra middlewares to wrap publishing process."), - ] = (), - ) -> "RedisMessage": - assert self._producer, NOT_CONNECTED_YET # nosec B101 - - kwargs = { - "stream": StreamSub.validate(stream or self.stream).name, - # basic args - "headers": headers or self.headers, - "correlation_id": correlation_id or gen_cor_id(), - "timeout": timeout, - } + _extra_middlewares: Iterable["PublisherMiddleware"], + ) -> None: + """This method should be called in subscriber flow only.""" + cmd = RedisPublishCommand.from_cmd(cmd) + + cmd.set_destination(stream=self.stream.name) - request: AsyncFunc = self._producer.request + cmd.add_headers(self.headers, override=False) + cmd.reply_to = cmd.reply_to or self.reply_to + cmd.maxlen = self.stream.maxlen - for pub_m in chain( - self._middlewares[::-1], - ( - _extra_middlewares - or (m(None).publish_scope for m in self._broker_middlewares[::-1]) - ), - ): - request = partial(pub_m, request) + await self._basic_publish(cmd, _extra_middlewares=_extra_middlewares) - published_msg = await request( + @override + async def request( + self, + message: "SendableMessage" = None, + stream: str | None = None, + *, + maxlen: int | None = None, + correlation_id: str | None = None, + headers: Optional["AnyDict"] = None, + timeout: float | None = 30.0, + ) -> "RedisMessage": + cmd = RedisPublishCommand( message, - **kwargs, + stream=stream or self.stream.name, + headers=self.headers | (headers or {}), + correlation_id=correlation_id or gen_cor_id(), + _publish_type=PublishType.REQUEST, + maxlen=maxlen or self.stream.maxlen, + timeout=timeout, ) - async with AsyncExitStack() as stack: - return_msg: Callable[[RedisMessage], Awaitable[RedisMessage]] = return_input - for m in self._broker_middlewares[::-1]: - mid = m(published_msg) - await stack.enter_async_context(mid) - return_msg = partial(mid.consume_scope, return_msg) - - parsed_msg = await self._producer._parser(published_msg) - parsed_msg._decoded_body = await self._producer._decoder(parsed_msg) - parsed_msg._source_type = SourceType.Response - return await return_msg(parsed_msg) - - raise AssertionError("unreachable") + msg: RedisMessage = await self._basic_request(cmd) + return msg diff --git a/faststream/redis/response.py b/faststream/redis/response.py index 9656fbc7b3..35d3263f36 100644 --- a/faststream/redis/response.py +++ b/faststream/redis/response.py @@ -1,11 +1,23 @@ -from typing import TYPE_CHECKING, Optional +from enum import Enum +from typing import TYPE_CHECKING, Optional, Union from typing_extensions import override -from faststream.broker.response import Response +from faststream.exceptions import SetupError +from faststream.redis.schemas import INCORRECT_SETUP_MSG +from faststream.response.publish_type import PublishType +from faststream.response.response import BatchPublishCommand, PublishCommand, Response if TYPE_CHECKING: - from faststream.types import AnyDict, SendableMessage + from redis.asyncio.client import Pipeline + + from faststream._internal.basic_types import AnyDict, SendableMessage + + +class DestinationType(str, Enum): + Channel = "channel" + List = "list" + Stream = "stream" class RedisResponse(Response): @@ -14,8 +26,8 @@ def __init__( body: Optional["SendableMessage"] = None, *, headers: Optional["AnyDict"] = None, - correlation_id: Optional[str] = None, - maxlen: Optional[int] = None, + correlation_id: str | None = None, + maxlen: int | None = None, ) -> None: super().__init__( body=body, @@ -25,9 +37,99 @@ def __init__( self.maxlen = maxlen @override - def as_publish_kwargs(self) -> "AnyDict": - publish_options = { - **super().as_publish_kwargs(), - "maxlen": self.maxlen, - } - return publish_options + def as_publish_command(self) -> "RedisPublishCommand": + return RedisPublishCommand( + self.body, + headers=self.headers, + correlation_id=self.correlation_id, + _publish_type=PublishType.PUBLISH, + # Kafka specific + channel="fake-channel", # it will be replaced by reply-sender + maxlen=self.maxlen, + ) + + +class RedisPublishCommand(BatchPublishCommand): + destination_type: DestinationType + + def __init__( + self, + message: "SendableMessage", + /, + *messages: "SendableMessage", + _publish_type: "PublishType", + correlation_id: str | None = None, + channel: str | None = None, + list: str | None = None, + stream: str | None = None, + maxlen: int | None = None, + headers: Optional["AnyDict"] = None, + reply_to: str = "", + timeout: float | None = 30.0, + pipeline: Optional["Pipeline[bytes]"] = None, + ) -> None: + super().__init__( + message, + *messages, + _publish_type=_publish_type, + correlation_id=correlation_id, + reply_to=reply_to, + destination="", + headers=headers, + ) + + self.pipeline = pipeline + + self.set_destination( + channel=channel, + list=list, + stream=stream, + ) + + # Stream option + self.maxlen = maxlen + + # Request option + self.timeout = timeout + + def set_destination( + self, + *, + channel: str | None = None, + list: str | None = None, + stream: str | None = None, + ) -> str: + if channel is not None: + self.destination_type = DestinationType.Channel + self.destination = channel + elif list is not None: + self.destination_type = DestinationType.List + self.destination = list + elif stream is not None: + self.destination_type = DestinationType.Stream + self.destination = stream + else: + raise SetupError(INCORRECT_SETUP_MSG) + + @classmethod + def from_cmd( + cls, + cmd: Union["PublishCommand", "RedisPublishCommand"], + *, + batch: bool = False, + ) -> "RedisPublishCommand": + if isinstance(cmd, RedisPublishCommand): + # NOTE: Should return a copy probably. + return cmd + + body, extra_bodies = cls._parse_bodies(cmd.body, batch=batch) + + return cls( + body, + *extra_bodies, + channel=cmd.destination, + correlation_id=cmd.correlation_id, + headers=cmd.headers, + reply_to=cmd.reply_to, + _publish_type=cmd.publish_type, + ) diff --git a/faststream/redis/router.py b/faststream/redis/router.py deleted file mode 100644 index ab625f8711..0000000000 --- a/faststream/redis/router.py +++ /dev/null @@ -1,267 +0,0 @@ -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Iterable, - Optional, - Sequence, - Union, -) - -from typing_extensions import Annotated, Doc, deprecated - -from faststream.broker.router import ArgsContainer, BrokerRouter, SubscriberRoute -from faststream.broker.utils import default_filter -from faststream.redis.broker.registrator import RedisRegistrator -from faststream.redis.message import BaseMessage - -if TYPE_CHECKING: - from fast_depends.dependencies import Depends - - from faststream.broker.types import ( - BrokerMiddleware, - CustomCallable, - Filter, - PublisherMiddleware, - SubscriberMiddleware, - ) - from faststream.redis.message import UnifyRedisMessage - from faststream.redis.schemas import ListSub, PubSub, StreamSub - from faststream.types import AnyDict, SendableMessage - - -class RedisPublisher(ArgsContainer): - """Delayed RedisPublisher registration object. - - Just a copy of RedisRegistrator.publisher(...) arguments. - """ - - def __init__( - self, - channel: Annotated[ - Union["PubSub", str, None], - Doc("Redis PubSub object name to send message."), - ] = None, - *, - list: Annotated[ - Union["ListSub", str, None], - Doc("Redis List object name to send message."), - ] = None, - stream: Annotated[ - Union["StreamSub", str, None], - Doc("Redis Stream object name to send message."), - ] = None, - headers: Annotated[ - Optional["AnyDict"], - Doc( - "Message headers to store metainformation. " - "Can be overridden by `publish.headers` if specified." - ), - ] = None, - reply_to: Annotated[ - str, - Doc("Reply message destination PubSub object name."), - ] = "", - middlewares: Annotated[ - Sequence["PublisherMiddleware"], - Doc("Publisher middlewares to wrap outgoing messages."), - ] = (), - # AsyncAPI information - title: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc("AsyncAPI publisher object description."), - ] = None, - schema: Annotated[ - Optional[Any], - Doc( - "AsyncAPI publishing message type. " - "Should be any python-native object annotation or `pydantic.BaseModel`." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - ) -> None: - super().__init__( - channel=channel, - list=list, - stream=stream, - headers=headers, - reply_to=reply_to, - middlewares=middlewares, - title=title, - description=description, - schema=schema, - include_in_schema=include_in_schema, - ) - - -class RedisRoute(SubscriberRoute): - """Class to store delayed RedisBroker subscriber registration.""" - - def __init__( - self, - call: Annotated[ - Union[ - Callable[..., "SendableMessage"], - Callable[..., Awaitable["SendableMessage"]], - ], - Doc( - "Message handler function " - "to wrap the same with `@broker.subscriber(...)` way." - ), - ], - channel: Annotated[ - Union["PubSub", str, None], - Doc("Redis PubSub object name to send message."), - ] = None, - *, - publishers: Annotated[ - Iterable["RedisPublisher"], - Doc("Redis publishers to broadcast the handler result."), - ] = (), - list: Annotated[ - Union["ListSub", str, None], - Doc("Redis List object name to send message."), - ] = None, - stream: Annotated[ - Union["StreamSub", str, None], - Doc("Redis Stream object name to send message."), - ] = None, - # broker arguments - dependencies: Annotated[ - Iterable["Depends"], - Doc("Dependencies list (`[Depends(),]`) to apply to the subscriber."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc( - "Parser to map original **aio_pika.IncomingMessage** Msg to FastStream one." - ), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - middlewares: Annotated[ - Sequence["SubscriberMiddleware[UnifyRedisMessage]"], - Doc("Subscriber middlewares to wrap incoming message processing."), - ] = (), - filter: Annotated[ - "Filter[UnifyRedisMessage]", - Doc( - "Overload subscriber to consume various messages from the same source." - ), - deprecated( - "Deprecated in **FastStream 0.5.0**. " - "Please, create `subscriber` object and use it explicitly instead. " - "Argument will be removed in **FastStream 0.6.0**." - ), - ] = default_filter, - retry: Annotated[ - bool, - Doc("Whether to `nack` message at processing exception."), - ] = False, - no_ack: Annotated[ - bool, - Doc("Whether to disable **FastStream** autoacknowledgement logic or not."), - ] = False, - no_reply: Annotated[ - bool, - Doc( - "Whether to disable **FastStream** RPC and Reply To auto responses or not." - ), - ] = False, - # AsyncAPI information - title: Annotated[ - Optional[str], - Doc("AsyncAPI subscriber object title."), - ] = None, - description: Annotated[ - Optional[str], - Doc( - "AsyncAPI subscriber object description. " - "Uses decorated docstring as default." - ), - ] = None, - include_in_schema: Annotated[ - bool, - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = True, - ) -> None: - super().__init__( - call, - channel=channel, - publishers=publishers, - list=list, - stream=stream, - dependencies=dependencies, - parser=parser, - decoder=decoder, - middlewares=middlewares, - filter=filter, - retry=retry, - no_ack=no_ack, - no_reply=no_reply, - title=title, - description=description, - include_in_schema=include_in_schema, - ) - - -class RedisRouter( - RedisRegistrator, - BrokerRouter[BaseMessage], -): - """Includable to RedisBroker router.""" - - def __init__( - self, - prefix: Annotated[ - str, - Doc("String prefix to add to all subscribers queues."), - ] = "", - handlers: Annotated[ - Iterable[RedisRoute], - Doc("Route object to include."), - ] = (), - *, - dependencies: Annotated[ - Iterable["Depends"], - Doc( - "Dependencies list (`[Depends(),]`) to apply to all routers' publishers/subscribers." - ), - ] = (), - middlewares: Annotated[ - Sequence["BrokerMiddleware[BaseMessage]"], - Doc("Router middlewares to apply to all routers' publishers/subscribers."), - ] = (), - parser: Annotated[ - Optional["CustomCallable"], - Doc("Parser to map original **IncomingMessage** Msg to FastStream one."), - ] = None, - decoder: Annotated[ - Optional["CustomCallable"], - Doc("Function to decode FastStream msg bytes body to python objects."), - ] = None, - include_in_schema: Annotated[ - Optional[bool], - Doc("Whetever to include operation in AsyncAPI schema or not."), - ] = None, - ) -> None: - super().__init__( - handlers=handlers, - # basic args - prefix=prefix, - dependencies=dependencies, - middlewares=middlewares, - parser=parser, - decoder=decoder, - include_in_schema=include_in_schema, - ) diff --git a/faststream/redis/schemas/list_sub.py b/faststream/redis/schemas/list_sub.py index f518f5cfb8..218409983f 100644 --- a/faststream/redis/schemas/list_sub.py +++ b/faststream/redis/schemas/list_sub.py @@ -1,7 +1,7 @@ +from copy import deepcopy from functools import cached_property -from typing import Optional -from faststream.broker.schemas import NameRequired +from faststream._internal.proto import NameRequired class ListSub(NameRequired): @@ -28,8 +28,10 @@ def __init__( self.polling_interval = polling_interval @cached_property - def records(self) -> Optional[int]: + def records(self) -> int | None: return self.max_records if self.batch else None - def __hash__(self) -> int: - return hash(f"list:{self.name}") + def add_prefix(self, prefix: str) -> "ListSub": + new_list = deepcopy(self) + new_list.name = f"{prefix}{new_list.name}" + return new_list diff --git a/faststream/redis/schemas/proto.py b/faststream/redis/schemas/proto.py index 2521a1a0a3..ede2f8f929 100644 --- a/faststream/redis/schemas/proto.py +++ b/faststream/redis/schemas/proto.py @@ -1,32 +1,24 @@ -from abc import abstractmethod -from typing import TYPE_CHECKING, Any, Union -from faststream.asyncapi.abc import AsyncAPIOperation -from faststream.exceptions import SetupError - -if TYPE_CHECKING: - from faststream.asyncapi.schema.bindings import redis - from faststream.redis.schemas import ListSub, PubSub, StreamSub +from faststream.exceptions import SetupError -class RedisAsyncAPIProtocol(AsyncAPIOperation): - @property - @abstractmethod - def channel_binding(self) -> "redis.ChannelBinding": ... - - @abstractmethod - def get_payloads(self) -> Any: ... +from .list_sub import ListSub +from .pub_sub import PubSub +from .stream_sub import StreamSub def validate_options( *, - channel: Union["PubSub", str, None], - list: Union["ListSub", str, None], - stream: Union["StreamSub", str, None], + channel: PubSub | str | None, + list: ListSub | str | None, + stream: StreamSub | str | None, ) -> None: if all((channel, list)): - raise SetupError("You can't use `PubSub` and `ListSub` both") - elif all((channel, stream)): - raise SetupError("You can't use `PubSub` and `StreamSub` both") - elif all((list, stream)): - raise SetupError("You can't use `ListSub` and `StreamSub` both") + msg = "You can't use `PubSub` and `ListSub` both" + raise SetupError(msg) + if all((channel, stream)): + msg = "You can't use `PubSub` and `StreamSub` both" + raise SetupError(msg) + if all((list, stream)): + msg = "You can't use `ListSub` and `StreamSub` both" + raise SetupError(msg) diff --git a/faststream/redis/schemas/pub_sub.py b/faststream/redis/schemas/pub_sub.py index 3026d6d2dc..8dd6313aef 100644 --- a/faststream/redis/schemas/pub_sub.py +++ b/faststream/redis/schemas/pub_sub.py @@ -1,5 +1,7 @@ -from faststream.broker.schemas import NameRequired -from faststream.utils.path import compile_path +from copy import deepcopy + +from faststream._internal.proto import NameRequired +from faststream._internal.utils.path import compile_path class PubSub(NameRequired): @@ -33,5 +35,7 @@ def __init__( self.pattern = channel if pattern else None self.polling_interval = polling_interval - def __hash__(self) -> int: - return hash(f"pubsub:{self.name}") + def add_prefix(self, prefix: str) -> "PubSub": + new_ch = deepcopy(self) + new_ch.name = f"{prefix}{new_ch.name}" + return new_ch diff --git a/faststream/redis/schemas/stream_sub.py b/faststream/redis/schemas/stream_sub.py index 70e2d9b3c5..f1dd469c47 100644 --- a/faststream/redis/schemas/stream_sub.py +++ b/faststream/redis/schemas/stream_sub.py @@ -1,7 +1,6 @@ -import warnings -from typing import Optional +from copy import deepcopy -from faststream.broker.schemas import NameRequired +from faststream._internal.proto import NameRequired from faststream.exceptions import SetupError @@ -23,24 +22,18 @@ class StreamSub(NameRequired): def __init__( self, stream: str, - polling_interval: Optional[int] = 100, - group: Optional[str] = None, - consumer: Optional[str] = None, + polling_interval: int | None = 100, + group: str | None = None, + consumer: str | None = None, batch: bool = False, no_ack: bool = False, - last_id: Optional[str] = None, - maxlen: Optional[int] = None, - max_records: Optional[int] = None, + last_id: str | None = None, + maxlen: int | None = None, + max_records: int | None = None, ) -> None: if (group and not consumer) or (not group and consumer): - raise SetupError("You should specify `group` and `consumer` both") - - if group and consumer and no_ack: - warnings.warn( - message="`no_ack` has no effect with consumer group", - category=RuntimeWarning, - stacklevel=1, - ) + msg = "You should specify `group` and `consumer` both" + raise SetupError(msg) if last_id is None: last_id = "$" @@ -56,9 +49,7 @@ def __init__( self.maxlen = maxlen self.max_records = max_records - def __hash__(self) -> int: - if self.group is not None: - return hash( - f"stream:{self.name} group:{self.group} consumer:{self.consumer}" - ) - return hash(f"stream:{self.name}") + def add_prefix(self, prefix: str) -> "StreamSub": + new_stream = deepcopy(self) + new_stream.name = f"{prefix}{new_stream.name}" + return new_stream diff --git a/faststream/redis/security.py b/faststream/redis/security.py index 08db65778d..b5ecbed20c 100644 --- a/faststream/redis/security.py +++ b/faststream/redis/security.py @@ -1,22 +1,25 @@ -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any from redis.asyncio.connection import Connection from faststream.security import BaseSecurity, SASLPlaintext if TYPE_CHECKING: - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict -def parse_security(security: Optional[BaseSecurity]) -> "AnyDict": +def parse_security(security: BaseSecurity | None) -> "AnyDict": if security is None: return {} - elif isinstance(security, SASLPlaintext): + + if isinstance(security, SASLPlaintext): return _parse_sasl_plaintext(security) - elif isinstance(security, BaseSecurity): + + if isinstance(security, BaseSecurity): return _parse_base_security(security) - else: - raise NotImplementedError(f"RedisBroker does not support {type(security)}") + + msg = f"RedisBroker does not support {type(security)}" + raise NotImplementedError(msg) def _parse_base_security(security: BaseSecurity) -> "AnyDict": @@ -38,8 +41,7 @@ def _connection_arguments(self) -> Any: } return {"connection_class": SSLConnection} - else: - return {} + return {} def _parse_sasl_plaintext(security: SASLPlaintext) -> "AnyDict": diff --git a/faststream/redis/subscriber/asyncapi.py b/faststream/redis/subscriber/asyncapi.py deleted file mode 100644 index 36171b247b..0000000000 --- a/faststream/redis/subscriber/asyncapi.py +++ /dev/null @@ -1,104 +0,0 @@ -from typing import Dict - -from faststream.asyncapi.schema import ( - Channel, - ChannelBinding, - CorrelationId, - Message, - Operation, -) -from faststream.asyncapi.schema.bindings import redis -from faststream.asyncapi.utils import resolve_payloads -from faststream.redis.schemas import ListSub, StreamSub -from faststream.redis.schemas.proto import RedisAsyncAPIProtocol -from faststream.redis.subscriber.usecase import ( - BatchListSubscriber, - BatchStreamSubscriber, - ChannelSubscriber, - ListSubscriber, - LogicSubscriber, - StreamSubscriber, -) - - -class AsyncAPISubscriber(LogicSubscriber, RedisAsyncAPIProtocol): - """A class to represent a Redis handler.""" - - def get_schema(self) -> Dict[str, Channel]: - payloads = self.get_payloads() - - return { - self.name: Channel( - description=self.description, - subscribe=Operation( - message=Message( - title=f"{self.name}:Message", - payload=resolve_payloads(payloads), - correlationId=CorrelationId( - location="$message.header#/correlation_id" - ), - ), - ), - bindings=ChannelBinding( - redis=self.channel_binding, - ), - ) - } - - -class AsyncAPIChannelSubscriber(ChannelSubscriber, AsyncAPISubscriber): - def get_name(self) -> str: - return f"{self.channel.name}:{self.call_name}" - - @property - def channel_binding(self) -> "redis.ChannelBinding": - return redis.ChannelBinding( - channel=self.channel.name, - method="psubscribe" if self.channel.pattern else "subscribe", - ) - - -class _StreamSubscriberMixin(AsyncAPISubscriber): - stream_sub: StreamSub - - def get_name(self) -> str: - return f"{self.stream_sub.name}:{self.call_name}" - - @property - def channel_binding(self) -> "redis.ChannelBinding": - return redis.ChannelBinding( - channel=self.stream_sub.name, - group_name=self.stream_sub.group, - consumer_name=self.stream_sub.consumer, - method="xreadgroup" if self.stream_sub.group else "xread", - ) - - -class AsyncAPIStreamSubscriber(StreamSubscriber, _StreamSubscriberMixin): - pass - - -class AsyncAPIStreamBatchSubscriber(BatchStreamSubscriber, _StreamSubscriberMixin): - pass - - -class _ListSubscriberMixin(AsyncAPISubscriber): - list_sub: ListSub - - def get_name(self) -> str: - return f"{self.list_sub.name}:{self.call_name}" - - @property - def channel_binding(self) -> "redis.ChannelBinding": - return redis.ChannelBinding( - channel=self.list_sub.name, - method="lpop", - ) - - -class AsyncAPIListSubscriber(ListSubscriber, _ListSubscriberMixin): - pass - - -class AsyncAPIListBatchSubscriber(BatchListSubscriber, _ListSubscriberMixin): - pass diff --git a/faststream/redis/subscriber/config.py b/faststream/redis/subscriber/config.py new file mode 100644 index 0000000000..094efe9255 --- /dev/null +++ b/faststream/redis/subscriber/config.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass, field + +from faststream._internal.configs import ( + SubscriberSpecificationConfig, + SubscriberUsecaseConfig, +) +from faststream._internal.constants import EMPTY +from faststream.middlewares.acknowledgement.conf import AckPolicy +from faststream.redis.configs import RedisBrokerConfig +from faststream.redis.schemas import ListSub, PubSub, StreamSub + + +class RedisSubscriberSpecificationConfig(SubscriberSpecificationConfig): + pass + + +@dataclass(kw_only=True) +class RedisSubscriberConfig(SubscriberUsecaseConfig): + _outer_config: RedisBrokerConfig + + list_sub: ListSub | None = field(default=None, repr=False) + channel_sub: PubSub | None = field(default=None, repr=False) + stream_sub: StreamSub | None = field(default=None, repr=False) + + _no_ack: bool = field(default_factory=lambda: EMPTY, repr=False) + + @property + def ack_policy(self) -> AckPolicy: + if self._no_ack is not EMPTY and self._no_ack: + return AckPolicy.DO_NOTHING + + if self.list_sub: + return AckPolicy.DO_NOTHING + + if self.channel_sub: + return AckPolicy.DO_NOTHING + + if self.stream_sub and (self.stream_sub.no_ack or not self.stream_sub.group): + return AckPolicy.DO_NOTHING + + if self._ack_policy is EMPTY: + return AckPolicy.REJECT_ON_ERROR + + return self._ack_policy diff --git a/faststream/redis/subscriber/factory.py b/faststream/redis/subscriber/factory.py index 9a43d054e8..bfa3ab9ca7 100644 --- a/faststream/redis/subscriber/factory.py +++ b/faststream/redis/subscriber/factory.py @@ -1,31 +1,46 @@ -from typing import TYPE_CHECKING, Iterable, Optional, Sequence, Union - -from typing_extensions import TypeAlias +import warnings +from typing import TYPE_CHECKING, TypeAlias, Union +from faststream._internal.constants import EMPTY +from faststream._internal.endpoint.subscriber.call_item import ( + CallsCollection, +) from faststream.exceptions import SetupError +from faststream.middlewares import AckPolicy from faststream.redis.schemas import INCORRECT_SETUP_MSG, ListSub, PubSub, StreamSub from faststream.redis.schemas.proto import validate_options -from faststream.redis.subscriber.asyncapi import ( - AsyncAPIChannelSubscriber, - AsyncAPIListBatchSubscriber, - AsyncAPIListSubscriber, - AsyncAPIStreamBatchSubscriber, - AsyncAPIStreamSubscriber, + +from .config import RedisSubscriberConfig, RedisSubscriberSpecificationConfig +from .specification import ( + ChannelSubscriberSpecification, + ListSubscriberSpecification, + RedisSubscriberSpecification, + StreamSubscriberSpecification, +) +from .usecases import ( + ChannelConcurrentSubscriber, + ChannelSubscriber, + ListBatchSubscriber, + ListConcurrentSubscriber, + ListSubscriber, + StreamBatchSubscriber, + StreamConcurrentSubscriber, + StreamSubscriber, ) if TYPE_CHECKING: - from fast_depends.dependencies import Depends - - from faststream.broker.types import BrokerMiddleware - from faststream.redis.message import UnifyRedisDict + from faststream.redis.configs import RedisBrokerConfig -SubsciberType: TypeAlias = Union[ - "AsyncAPIChannelSubscriber", - "AsyncAPIStreamBatchSubscriber", - "AsyncAPIStreamSubscriber", - "AsyncAPIListBatchSubscriber", - "AsyncAPIListSubscriber", -] +SubsciberType: TypeAlias = ( + ChannelSubscriber + | StreamBatchSubscriber + | StreamSubscriber + | ListBatchSubscriber + | ListSubscriber + | ChannelConcurrentSubscriber + | ListConcurrentSubscriber + | StreamConcurrentSubscriber +) def create_subscriber( @@ -34,92 +49,138 @@ def create_subscriber( list: Union["ListSub", str, None], stream: Union["StreamSub", str, None], # Subscriber args - no_ack: bool = False, + ack_policy: "AckPolicy", + no_ack: bool, + config: "RedisBrokerConfig", no_reply: bool = False, - retry: bool = False, - broker_dependencies: Iterable["Depends"] = (), - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"] = (), # AsyncAPI args - title_: Optional[str] = None, - description_: Optional[str] = None, + title_: str | None = None, + description_: str | None = None, include_in_schema: bool = True, + max_workers: int = 1, ) -> SubsciberType: - validate_options(channel=channel, list=list, stream=stream) + _validate_input_for_misconfigure( + channel=channel, + list=list, + stream=stream, + ack_policy=ack_policy, + no_ack=no_ack, + max_workers=max_workers, + ) + + subscriber_config = RedisSubscriberConfig( + channel_sub=PubSub.validate(channel), + list_sub=ListSub.validate(list), + stream_sub=StreamSub.validate(stream), + no_reply=no_reply, + _outer_config=config, + _ack_policy=ack_policy, + ) + + specification_config = RedisSubscriberSpecificationConfig( + title_=title_, + description_=description_, + include_in_schema=include_in_schema, + ) - if (channel_sub := PubSub.validate(channel)) is not None: - return AsyncAPIChannelSubscriber( - channel=channel_sub, - # basic args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, + calls = CallsCollection() + + specification: RedisSubscriberSpecification + if subscriber_config.channel_sub: + specification = ChannelSubscriberSpecification( + config, specification_config, calls, channel=subscriber_config.channel_sub, ) - elif (stream_sub := StreamSub.validate(stream)) is not None: - if stream_sub.batch: - return AsyncAPIStreamBatchSubscriber( - stream=stream_sub, - # basic args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, + subscriber_config._ack_policy = AckPolicy.DO_NOTHING + + if max_workers > 1: + return ChannelConcurrentSubscriber( + subscriber_config, + specification, calls, + max_workers=max_workers, ) - else: - return AsyncAPIStreamSubscriber( - stream=stream_sub, - # basic args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, + + return ChannelSubscriber(subscriber_config, + specification, calls) + + if subscriber_config.stream_sub: + specification = StreamSubscriberSpecification( + config, specification_config, calls, stream_sub=subscriber_config.stream_sub + ) + + if subscriber_config.stream_sub.batch: + return StreamBatchSubscriber(subscriber_config, + specification, calls) + + if max_workers > 1: + return StreamConcurrentSubscriber( + subscriber_config, + specification, calls, + max_workers=max_workers, ) - elif (list_sub := ListSub.validate(list)) is not None: - if list_sub.batch: - return AsyncAPIListBatchSubscriber( - list=list_sub, - # basic args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, + return StreamSubscriber(subscriber_config, + specification, calls) + + if subscriber_config.list_sub: + specification = ListSubscriberSpecification( + config, specification_config, calls, list_sub=subscriber_config.list_sub + ) + + if subscriber_config.list_sub.batch: + return ListBatchSubscriber(subscriber_config, + specification, calls) + + if max_workers > 1: + return ListConcurrentSubscriber( + subscriber_config, + specification, calls, + max_workers=max_workers, ) - else: - return AsyncAPIListSubscriber( - list=list_sub, - # basic args - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_dependencies=broker_dependencies, - broker_middlewares=broker_middlewares, - # AsyncAPI args - title_=title_, - description_=description_, - include_in_schema=include_in_schema, + + return ListSubscriber(subscriber_config, + specification, calls) + + raise SetupError(INCORRECT_SETUP_MSG) + + +def _validate_input_for_misconfigure( + *, + channel: Union["PubSub", str, None], + list: Union["ListSub", str, None], + stream: Union["StreamSub", str, None], + ack_policy: AckPolicy, + no_ack: bool, + max_workers: int, +) -> None: + validate_options(channel=channel, list=list, stream=stream) + + if no_ack is not EMPTY: + warnings.warn( + "`no_ack` option was deprecated in prior to `ack_policy=AckPolicy.DO_NOTHING`. Scheduled to remove in 0.7.0", + category=DeprecationWarning, + stacklevel=4, + ) + + if ack_policy is not EMPTY: + msg = "You can't use deprecated `no_ack` and `ack_policy` simultaneously. Please, use `ack_policy` only." + raise SetupError(msg) + + if stream and no_ack and max_workers > 1: + msg = "Max workers not work with manual no_ack mode." + raise SetupError(msg) + + if ack_policy is not EMPTY: + if channel: + warnings.warn( + "You can't use acknowledgement policy with PubSub subscriber.", + RuntimeWarning, + stacklevel=4, ) - else: - raise SetupError(INCORRECT_SETUP_MSG) + if list: + warnings.warn( + "You can't use acknowledgement policy with List subscriber.", + RuntimeWarning, + stacklevel=4, + ) diff --git a/faststream/redis/subscriber/specification.py b/faststream/redis/subscriber/specification.py new file mode 100644 index 0000000000..d5b1f599e0 --- /dev/null +++ b/faststream/redis/subscriber/specification.py @@ -0,0 +1,134 @@ +from typing import TYPE_CHECKING, Any + +from faststream._internal.endpoint.subscriber import SubscriberSpecification +from faststream.redis.configs import RedisBrokerConfig +from faststream.redis.schemas import ListSub, PubSub, StreamSub +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 .config import RedisSubscriberSpecificationConfig + +if TYPE_CHECKING: + from faststream._internal.endpoint.subscriber.call_item import ( + CallsCollection, + ) + + +class RedisSubscriberSpecification( + SubscriberSpecification[RedisBrokerConfig, RedisSubscriberSpecificationConfig] +): + def get_schema(self) -> dict[str, SubscriberSpec]: + payloads = self.get_payloads() + + return { + self.name: SubscriberSpec( + description=self.description, + operation=Operation( + message=Message( + title=f"{self.name}:Message", + payload=resolve_payloads(payloads), + ), + bindings=None, + ), + bindings=ChannelBinding( + redis=self.channel_binding, + ), + ), + } + + @property + def channel_binding(self) -> redis.ChannelBinding: + raise NotImplementedError + + +class ChannelSubscriberSpecification(RedisSubscriberSpecification): + def __init__( + self, + _outer_config: "RedisBrokerConfig", + specification_config: "RedisSubscriberSpecificationConfig", + calls: "CallsCollection[Any]", + channel: PubSub, + ) -> None: + super().__init__(_outer_config, specification_config, calls) + self.channel = channel + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{self.channel_name}:{self.call_name}" + + @property + def channel_name(self) -> str: + return f"{self._outer_config.prefix}{self.channel.name}" + + @property + def channel_binding(self) -> "redis.ChannelBinding": + return redis.ChannelBinding( + channel=self.channel_name, + method="psubscribe" if self.channel.pattern else "subscribe", + ) + + +class ListSubscriberSpecification(RedisSubscriberSpecification): + def __init__( + self, + _outer_config: "RedisBrokerConfig", + specification_config: "RedisSubscriberSpecificationConfig", + calls: "CallsCollection[Any]", + list_sub: ListSub, + ) -> None: + super().__init__(_outer_config, specification_config, calls) + self.list_sub = list_sub + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{self.list_name}:{self.call_name}" + + @property + def list_name(self) -> str: + return f"{self._outer_config.prefix}{self.list_sub.name}" + + @property + def channel_binding(self) -> "redis.ChannelBinding": + return redis.ChannelBinding( + channel=self.list_name, + method="lpop", + ) + + +class StreamSubscriberSpecification(RedisSubscriberSpecification): + def __init__( + self, + _outer_config: "RedisBrokerConfig", + specification_config: "RedisSubscriberSpecificationConfig", + calls: "CallsCollection[Any]", + stream_sub: StreamSub, + ) -> None: + super().__init__(_outer_config, specification_config, calls) + self.stream_sub = stream_sub + + @property + def name(self) -> str: + if self.config.title_: + return self.config.title_ + + return f"{self.stream_name}:{self.call_name}" + + @property + def stream_name(self) -> str: + return f"{self._outer_config.prefix}{self.stream_sub.name}" + + @property + def channel_binding(self) -> "redis.ChannelBinding": + return redis.ChannelBinding( + channel=self.stream_name, + group_name=self.stream_sub.group, + consumer_name=self.stream_sub.consumer, + method="xreadgroup" if self.stream_sub.group else "xread", + ) diff --git a/faststream/redis/subscriber/usecase.py b/faststream/redis/subscriber/usecase.py deleted file mode 100644 index 1ebf7ec4a1..0000000000 --- a/faststream/redis/subscriber/usecase.py +++ /dev/null @@ -1,882 +0,0 @@ -import asyncio -import math -from abc import abstractmethod -from contextlib import suppress -from copy import deepcopy -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Dict, - Iterable, - List, - Optional, - Sequence, - Tuple, - cast, -) - -import anyio -from redis.asyncio.client import PubSub as RPubSub -from redis.asyncio.client import Redis -from redis.exceptions import ResponseError -from typing_extensions import TypeAlias, override - -from faststream.broker.publisher.fake import FakePublisher -from faststream.broker.subscriber.usecase import SubscriberUsecase -from faststream.broker.utils import process_msg -from faststream.redis.message import ( - BatchListMessage, - BatchStreamMessage, - DefaultListMessage, - DefaultStreamMessage, - PubSubMessage, - RedisListMessage, - RedisMessage, - RedisStreamMessage, - UnifyRedisDict, -) -from faststream.redis.parser import ( - RedisBatchListParser, - RedisBatchStreamParser, - RedisListParser, - RedisPubSubParser, - RedisStreamParser, -) -from faststream.redis.schemas import ListSub, PubSub, StreamSub - -if TYPE_CHECKING: - from fast_depends.dependencies import Depends - - from faststream.broker.message import StreamMessage as BrokerStreamMessage - from faststream.broker.publisher.proto import ProducerProto - from faststream.broker.types import ( - AsyncCallable, - BrokerMiddleware, - CustomCallable, - ) - from faststream.types import AnyDict, Decorator, LoggerProto - - -TopicName: TypeAlias = bytes -Offset: TypeAlias = bytes - - -class LogicSubscriber(SubscriberUsecase[UnifyRedisDict]): - """A class to represent a Redis handler.""" - - _client: Optional["Redis[bytes]"] - - def __init__( - self, - *, - default_parser: "AsyncCallable", - default_decoder: "AsyncCallable", - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - super().__init__( - default_parser=default_parser, - default_decoder=default_decoder, - # Propagated options - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - self._client = None - self.task: Optional[asyncio.Task[None]] = None - - @override - def setup( # type: ignore[override] - self, - *, - connection: Optional["Redis[bytes]"], - # basic args - logger: Optional["LoggerProto"], - producer: Optional["ProducerProto"], - graceful_timeout: Optional[float], - extra_context: "AnyDict", - # broker options - broker_parser: Optional["CustomCallable"], - broker_decoder: Optional["CustomCallable"], - # dependant args - apply_types: bool, - is_validate: bool, - _get_dependant: Optional[Callable[..., Any]], - _call_decorators: Iterable["Decorator"], - ) -> None: - self._client = connection - - super().setup( - logger=logger, - producer=producer, - graceful_timeout=graceful_timeout, - extra_context=extra_context, - broker_parser=broker_parser, - broker_decoder=broker_decoder, - apply_types=apply_types, - is_validate=is_validate, - _get_dependant=_get_dependant, - _call_decorators=_call_decorators, - ) - - def _make_response_publisher( - self, - message: "BrokerStreamMessage[UnifyRedisDict]", - ) -> Sequence[FakePublisher]: - if self._producer is None: - return () - - return ( - FakePublisher( - self._producer.publish, - publish_kwargs={ - "channel": message.reply_to, - }, - ), - ) - - @override - async def start( - self, - *args: Any, - ) -> None: - if self.task: - return - - await super().start() - - start_signal = anyio.Event() - - if self.calls: - self.task = asyncio.create_task( - self._consume(*args, start_signal=start_signal) - ) - - with anyio.fail_after(3.0): - await start_signal.wait() - - else: - start_signal.set() - - async def _consume(self, *args: Any, start_signal: anyio.Event) -> None: - connected = True - - while self.running: - try: - await self._get_msgs(*args) - - except Exception: # noqa: PERF203 - if connected: - connected = False - await anyio.sleep(5) - - else: - if not connected: - connected = True - - finally: - if not start_signal.is_set(): - with suppress(Exception): - start_signal.set() - - @abstractmethod - async def _get_msgs(self, *args: Any) -> None: - raise NotImplementedError() - - async def close(self) -> None: - await super().close() - - if self.task is not None and not self.task.done(): - self.task.cancel() - self.task = None - - @staticmethod - def build_log_context( - message: Optional["BrokerStreamMessage[Any]"], - channel: str = "", - ) -> Dict[str, str]: - return { - "channel": channel, - "message_id": getattr(message, "message_id", ""), - } - - -class ChannelSubscriber(LogicSubscriber): - subscription: Optional[RPubSub] - - def __init__( - self, - *, - channel: "PubSub", - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - parser = RedisPubSubParser(pattern=channel.path_regex) - super().__init__( - default_parser=parser.parse_message, - default_decoder=parser.decode_message, - # Propagated options - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - self.channel = channel - self.subscription = None - - def __hash__(self) -> int: - return hash(self.channel) - - def get_log_context( - self, - message: Optional["BrokerStreamMessage[Any]"], - ) -> Dict[str, str]: - return self.build_log_context( - message=message, - channel=self.channel.name, - ) - - @override - async def start(self) -> None: - if self.subscription: - return - - assert self._client, "You should setup subscriber at first." # nosec B101 - - self.subscription = psub = self._client.pubsub() - - if self.channel.pattern: - await psub.psubscribe(self.channel.name) - else: - await psub.subscribe(self.channel.name) - - await super().start(psub) - - async def close(self) -> None: - if self.subscription is not None: - await self.subscription.unsubscribe() - await self.subscription.aclose() # type: ignore[attr-defined] - self.subscription = None - - await super().close() - - @override - async def get_one( # type: ignore[override] - self, - *, - timeout: float = 5.0, - ) -> "Optional[RedisMessage]": - assert self.subscription, "You should start subscriber at first." # nosec B101 - assert ( # nosec B101 - not self.calls - ), "You can't use `get_one` method if subscriber has registered handlers." - - sleep_interval = timeout / 10 - - message: Optional[PubSubMessage] = None - - with anyio.move_on_after(timeout): - while (message := await self._get_message(self.subscription)) is None: # noqa: ASYNC110 - await anyio.sleep(sleep_interval) - - msg: Optional[RedisMessage] = await process_msg( # type: ignore[assignment] - msg=message, - middlewares=self._broker_middlewares, # type: ignore[arg-type] - parser=self._parser, - decoder=self._decoder, - ) - return msg - - async def _get_message(self, psub: RPubSub) -> Optional[PubSubMessage]: - raw_msg = await psub.get_message( - ignore_subscribe_messages=True, - timeout=self.channel.polling_interval, - ) - - if raw_msg: - return PubSubMessage( - type=raw_msg["type"], - data=raw_msg["data"], - channel=raw_msg["channel"].decode(), - pattern=raw_msg["pattern"], - ) - - return None - - async def _get_msgs(self, psub: RPubSub) -> None: - if msg := await self._get_message(psub): - await self.consume(msg) # type: ignore[arg-type] - - def add_prefix(self, prefix: str) -> None: - new_ch = deepcopy(self.channel) - new_ch.name = "".join((prefix, new_ch.name)) - self.channel = new_ch - - -class _ListHandlerMixin(LogicSubscriber): - def __init__( - self, - *, - list: ListSub, - default_parser: "AsyncCallable", - default_decoder: "AsyncCallable", - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - super().__init__( - default_parser=default_parser, - default_decoder=default_decoder, - # Propagated options - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - self.list_sub = list - - def __hash__(self) -> int: - return hash(self.list_sub) - - def get_log_context( - self, - message: Optional["BrokerStreamMessage[Any]"], - ) -> Dict[str, str]: - return self.build_log_context( - message=message, - channel=self.list_sub.name, - ) - - @override - async def _consume( # type: ignore[override] - self, - client: "Redis[bytes]", - *, - start_signal: "anyio.Event", - ) -> None: - if await client.ping(): - start_signal.set() - await super()._consume(client, start_signal=start_signal) - - @override - async def start(self) -> None: - if self.task: - return - - assert self._client, "You should setup subscriber at first." # nosec B101 - - await super().start(self._client) - - @override - async def get_one( # type: ignore[override] - self, - *, - timeout: float = 5.0, - ) -> "Optional[RedisListMessage]": - assert self._client, "You should start subscriber at first." # nosec B101 - assert ( # nosec B101 - not self.calls - ), "You can't use `get_one` method if subscriber has registered handlers." - - sleep_interval = timeout / 10 - raw_message = None - - with anyio.move_on_after(timeout): - while ( # noqa: ASYNC110 - raw_message := await self._client.lpop(name=self.list_sub.name) - ) is None: - await anyio.sleep(sleep_interval) - - if not raw_message: - return None - - msg: RedisListMessage = await process_msg( # type: ignore[assignment] - msg=DefaultListMessage( - type="list", - data=raw_message, - channel=self.list_sub.name, - ), - middlewares=self._broker_middlewares, # type: ignore[arg-type] - parser=self._parser, - decoder=self._decoder, - ) - return msg - - def add_prefix(self, prefix: str) -> None: - new_list = deepcopy(self.list_sub) - new_list.name = "".join((prefix, new_list.name)) - self.list_sub = new_list - - -class ListSubscriber(_ListHandlerMixin): - def __init__( - self, - *, - list: ListSub, - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - parser = RedisListParser() - super().__init__( - list=list, - default_parser=parser.parse_message, - default_decoder=parser.decode_message, - # Propagated options - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - 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: - raw_msg = await client.lpop(name=self.list_sub.name) - - if raw_msg: - msg = DefaultListMessage( - type="list", - data=raw_msg, - channel=self.list_sub.name, - ) - - await self.consume(msg) # type: ignore[arg-type] - - else: - await anyio.sleep(self.list_sub.polling_interval) - - -class BatchListSubscriber(_ListHandlerMixin): - def __init__( - self, - *, - list: ListSub, - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - parser = RedisBatchListParser() - super().__init__( - list=list, - default_parser=parser.parse_message, - default_decoder=parser.decode_message, - # Propagated options - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - 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: - raw_msgs = await client.lpop( - name=self.list_sub.name, - count=self.list_sub.max_records, - ) - - if raw_msgs: - msg = BatchListMessage( - type="blist", - channel=self.list_sub.name, - data=raw_msgs, - ) - - await self.consume(msg) # type: ignore[arg-type] - - else: - await anyio.sleep(self.list_sub.polling_interval) - - -class _StreamHandlerMixin(LogicSubscriber): - def __init__( - self, - *, - stream: StreamSub, - default_parser: "AsyncCallable", - default_decoder: "AsyncCallable", - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - super().__init__( - default_parser=default_parser, - default_decoder=default_decoder, - # Propagated options - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - self.stream_sub = stream - self.last_id = stream.last_id - - def __hash__(self) -> int: - return hash(self.stream_sub) - - def get_log_context( - self, - message: Optional["BrokerStreamMessage[Any]"], - ) -> Dict[str, str]: - return self.build_log_context( - message=message, - channel=self.stream_sub.name, - ) - - @override - async def _consume(self, *args: Any, start_signal: anyio.Event) -> None: - self._client = cast("Redis[bytes]", self._client) - if await self._client.ping(): - start_signal.set() - await super()._consume(*args, start_signal=start_signal) - - @override - async def start(self) -> None: - if self.task: - return - - assert self._client, "You should setup subscriber at first." # nosec B101 - - client = self._client - - self.extra_watcher_options.update( - redis=client, - group=self.stream_sub.group, - ) - - stream = self.stream_sub - - read: Callable[ - [str], - Awaitable[ - Tuple[ - Tuple[ - TopicName, - Tuple[ - Tuple[ - Offset, - Dict[bytes, bytes], - ], - ..., - ], - ], - ..., - ], - ], - ] - - if stream.group and stream.consumer: - try: - await client.xgroup_create( - name=stream.name, - id=self.last_id, - groupname=stream.group, - mkstream=True, - ) - except ResponseError as e: - if "already exists" not in str(e): - raise e - - def read( - _: str, - ) -> Awaitable[ - Tuple[ - Tuple[ - TopicName, - Tuple[ - Tuple[ - Offset, - Dict[bytes, bytes], - ], - ..., - ], - ], - ..., - ], - ]: - return client.xreadgroup( - groupname=stream.group, - consumername=stream.consumer, - streams={stream.name: ">"}, - count=stream.max_records, - block=stream.polling_interval, - noack=stream.no_ack, - ) - - else: - - def read( - last_id: str, - ) -> Awaitable[ - Tuple[ - Tuple[ - TopicName, - Tuple[ - Tuple[ - Offset, - Dict[bytes, bytes], - ], - ..., - ], - ], - ..., - ], - ]: - return client.xread( - {stream.name: last_id}, - block=stream.polling_interval, - count=stream.max_records, - ) - - await super().start(read) - - @override - async def get_one( # type: ignore[override] - self, - *, - timeout: float = 5.0, - ) -> "Optional[RedisStreamMessage]": - assert self._client, "You should start subscriber at first." # nosec B101 - assert ( # nosec B101 - not self.calls - ), "You can't use `get_one` method if subscriber has registered handlers." - - stream_message = await self._client.xread( - {self.stream_sub.name: self.last_id}, - block=math.ceil(timeout * 1000), - count=1, - ) - - if not stream_message: - return None - - ((stream_name, ((message_id, raw_message),)),) = stream_message - - self.last_id = message_id.decode() - - msg: RedisStreamMessage = await process_msg( # type: ignore[assignment] - msg=DefaultStreamMessage( - type="stream", - channel=stream_name.decode(), - message_ids=[message_id], - data=raw_message, - ), - middlewares=self._broker_middlewares, # type: ignore[arg-type] - parser=self._parser, - decoder=self._decoder, - ) - return msg - - def add_prefix(self, prefix: str) -> None: - new_stream = deepcopy(self.stream_sub) - new_stream.name = "".join((prefix, new_stream.name)) - self.stream_sub = new_stream - - -class StreamSubscriber(_StreamHandlerMixin): - def __init__( - self, - *, - stream: StreamSub, - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - parser = RedisStreamParser() - super().__init__( - stream=stream, - default_parser=parser.parse_message, - default_decoder=parser.decode_message, - # Propagated options - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - async def _get_msgs( - self, - read: Callable[ - [str], - Awaitable[ - Tuple[ - Tuple[ - TopicName, - Tuple[ - Tuple[ - Offset, - Dict[bytes, bytes], - ], - ..., - ], - ], - ..., - ], - ], - ], - ) -> None: - for stream_name, msgs in await read(self.last_id): - if msgs: - self.last_id = msgs[-1][0].decode() - - for message_id, raw_msg in msgs: - msg = DefaultStreamMessage( - type="stream", - channel=stream_name.decode(), - message_ids=[message_id], - data=raw_msg, - ) - - await self.consume(msg) # type: ignore[arg-type] - - -class BatchStreamSubscriber(_StreamHandlerMixin): - def __init__( - self, - *, - stream: StreamSub, - # Subscriber args - no_ack: bool, - no_reply: bool, - retry: bool, - broker_dependencies: Iterable["Depends"], - broker_middlewares: Sequence["BrokerMiddleware[UnifyRedisDict]"], - # AsyncAPI args - title_: Optional[str], - description_: Optional[str], - include_in_schema: bool, - ) -> None: - parser = RedisBatchStreamParser() - super().__init__( - stream=stream, - default_parser=parser.parse_message, - default_decoder=parser.decode_message, - # Propagated options - no_ack=no_ack, - no_reply=no_reply, - retry=retry, - broker_middlewares=broker_middlewares, - broker_dependencies=broker_dependencies, - # AsyncAPI - title_=title_, - description_=description_, - include_in_schema=include_in_schema, - ) - - async def _get_msgs( - self, - read: Callable[ - [str], - Awaitable[ - Tuple[Tuple[bytes, Tuple[Tuple[bytes, Dict[bytes, bytes]], ...]], ...], - ], - ], - ) -> None: - for stream_name, msgs in await read(self.last_id): - if msgs: - self.last_id = msgs[-1][0].decode() - - data: List[Dict[bytes, bytes]] = [] - ids: List[bytes] = [] - for message_id, i in msgs: - data.append(i) - ids.append(message_id) - - msg = BatchStreamMessage( - type="bstream", - channel=stream_name.decode(), - data=data, - message_ids=ids, - ) - - await self.consume(msg) # type: ignore[arg-type] diff --git a/faststream/redis/subscriber/usecases/__init__.py b/faststream/redis/subscriber/usecases/__init__.py new file mode 100644 index 0000000000..16c9a13e28 --- /dev/null +++ b/faststream/redis/subscriber/usecases/__init__.py @@ -0,0 +1,24 @@ +from .basic import LogicSubscriber +from .channel_subscriber import ChannelConcurrentSubscriber, ChannelSubscriber +from .list_subscriber import ( + ListBatchSubscriber, + ListConcurrentSubscriber, + ListSubscriber, +) +from .stream_subscriber import ( + StreamBatchSubscriber, + StreamConcurrentSubscriber, + StreamSubscriber, +) + +__all__ = ( + "ChannelConcurrentSubscriber", + "ChannelSubscriber", + "ListBatchSubscriber", + "ListConcurrentSubscriber", + "ListSubscriber", + "LogicSubscriber", + "StreamBatchSubscriber", + "StreamConcurrentSubscriber", + "StreamSubscriber", +) diff --git a/faststream/redis/subscriber/usecases/basic.py b/faststream/redis/subscriber/usecases/basic.py new file mode 100644 index 0000000000..8a290d7c93 --- /dev/null +++ b/faststream/redis/subscriber/usecases/basic.py @@ -0,0 +1,142 @@ +import logging +from abc import abstractmethod +from collections.abc import Sequence +from contextlib import suppress +from typing import ( + TYPE_CHECKING, + Any, + Optional, + TypeAlias, +) + +import anyio +from typing_extensions import override + +from faststream._internal.endpoint.subscriber.mixins import ConcurrentMixin, TasksMixin +from faststream._internal.endpoint.subscriber.usecase import SubscriberUsecase +from faststream.redis.message import ( + UnifyRedisDict, +) +from faststream.redis.publisher.fake import RedisFakePublisher + +if TYPE_CHECKING: + from redis.asyncio.client import Redis + + from faststream._internal.endpoint.publisher import BasePublisherProto + from faststream._internal.endpoint.subscriber.call_item import ( + CallsCollection, + ) + from faststream.message import StreamMessage as BrokerStreamMessage + from faststream.redis.configs import RedisBrokerConfig + from faststream.redis.subscriber.config import RedisSubscriberConfig + from faststream.redis.subscriber.specification import RedisSubscriberSpecification + + +TopicName: TypeAlias = bytes +Offset: TypeAlias = bytes + + +class LogicSubscriber(TasksMixin, SubscriberUsecase[UnifyRedisDict]): + """A class to represent a Redis handler.""" + + _outer_config: "RedisBrokerConfig" + + @property + def _client(self) -> "Redis[bytes]": + return self._outer_config.connection.client + + def _make_response_publisher( + self, + message: "BrokerStreamMessage[UnifyRedisDict]", + ) -> Sequence["BasePublisherProto"]: + return ( + RedisFakePublisher( + self._outer_config.producer, + channel=message.reply_to, + ), + ) + + @override + async def start( + self, + *args: Any, + ) -> None: + if self.tasks: + return + + await super().start() + + self._post_start() + + start_signal = anyio.Event() + + if self.calls: + self.add_task(self._consume(*args, start_signal=start_signal)) + + with anyio.fail_after(3.0): + await start_signal.wait() + + else: + start_signal.set() + + async def _consume(self, *args: Any, start_signal: anyio.Event) -> None: + connected = True + + while self.running: + try: + await self._get_msgs(*args) + + except Exception as e: # noqa: PERF203 + self._log( + log_level=logging.ERROR, + message="Message fetch error", + exc_info=e, + ) + + if connected: + connected = False + + await anyio.sleep(5) + + else: + if not connected: + connected = True + + finally: + if not start_signal.is_set(): + with suppress(Exception): + start_signal.set() + + @abstractmethod + async def _get_msgs(self, *args: Any) -> None: + raise NotImplementedError + + @staticmethod + def build_log_context( + message: Optional["BrokerStreamMessage[Any]"], + channel: str = "", + ) -> dict[str, str]: + return { + "channel": channel, + "message_id": getattr(message, "message_id", ""), + } + + async def consume_one(self, msg: "BrokerStreamMessage") -> None: + await self.consume(msg) + + +class ConcurrentSubscriber(ConcurrentMixin["BrokerStreamMessage"], LogicSubscriber): + def __init__( + self, config: "RedisSubscriberConfig", specification: "RedisSubscriberSpecification", calls: "CallsCollection[Any]", + max_workers: int, + ) -> None: + super().__init__(config, specification, calls, max_workers=max_workers) + + self._client = None + + async def start(self) -> None: + await super().start() + self.start_consume_task() + + async def consume_one(self, msg: "BrokerStreamMessage") -> None: + await self._put_msg(msg) diff --git a/faststream/redis/subscriber/usecases/channel_subscriber.py b/faststream/redis/subscriber/usecases/channel_subscriber.py new file mode 100644 index 0000000000..c70780cebe --- /dev/null +++ b/faststream/redis/subscriber/usecases/channel_subscriber.py @@ -0,0 +1,187 @@ +from collections.abc import AsyncIterator +from typing import ( + TYPE_CHECKING, + Any, + Optional, + TypeAlias, +) + +import anyio +from redis.asyncio.client import ( + PubSub as RPubSub, +) +from typing_extensions import override + +from faststream._internal.endpoint.subscriber.mixins import ConcurrentMixin +from faststream._internal.endpoint.utils import process_msg +from faststream.redis.message import ( + PubSubMessage, + RedisMessage, +) +from faststream.redis.parser import ( + RedisPubSubParser, +) + +from .basic import LogicSubscriber + +if TYPE_CHECKING: + from faststream._internal.endpoint.subscriber.call_item import ( + CallsCollection, + ) + from faststream.message import StreamMessage as BrokerStreamMessage + from faststream.redis.schemas import PubSub + from faststream.redis.subscriber.config import RedisSubscriberConfig + from faststream.redis.subscriber.specification import RedisSubscriberSpecification + + +TopicName: TypeAlias = bytes +Offset: TypeAlias = bytes + + +class ChannelSubscriber(LogicSubscriber): + subscription: RPubSub | None + + def __init__(self, config: "RedisSubscriberConfig", specification: "RedisSubscriberSpecification", calls: "CallsCollection[Any]") -> None: + assert config.channel_sub # nosec B101 + parser = RedisPubSubParser(pattern=config.channel_sub.path_regex) + config.decoder = parser.decode_message + config.parser = parser.parse_message + super().__init__(config, specification, calls) + + self._channel = config.channel_sub + self.subscription = None + + @property + def channel(self) -> "PubSub": + return self._channel.add_prefix(self._outer_config.prefix) + + def get_log_context( + self, + message: Optional["BrokerStreamMessage[Any]"], + ) -> dict[str, str]: + return self.build_log_context( + message=message, + channel=self.channel.name, + ) + + @override + async def start(self) -> None: + if self.subscription: + return + + assert self._client, "You should setup subscriber at first." # nosec B101 + + self.subscription = psub = self._client.pubsub() + + if self.channel.pattern: + await psub.psubscribe(self.channel.name) + else: + await psub.subscribe(self.channel.name) + + await super().start(psub) + + async def close(self) -> None: + await super().close() + + if self.subscription is not None: + await self.subscription.unsubscribe() + await self.subscription.aclose() # type: ignore[attr-defined] + self.subscription = None + + @override + async def get_one( + self, + *, + timeout: float = 5.0, + ) -> "RedisMessage | None": + assert self.subscription, "You should start subscriber at first." # nosec B101 + assert ( # nosec B101 + not self.calls + ), "You can't use `get_one` method if subscriber has registered handlers." + + sleep_interval = timeout / 10 + + raw_message: PubSubMessage | None = None + + with anyio.move_on_after(timeout): + while (raw_message := await self._get_message(self.subscription)) is None: # noqa: ASYNC110 + await anyio.sleep(sleep_interval) + + context = self._outer_config.fd_config.context + + msg: RedisMessage | None = await process_msg( # type: ignore[assignment] + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + return msg + + @override + # type: ignore[override] + async def __aiter__(self) -> AsyncIterator["RedisMessage"]: + assert self.subscription, "You should start subscriber at first." # nosec B101 + assert ( # nosec B101 + not self.calls + ), "You can't use iterator if subscriber has registered handlers." + + timeout = 5 + sleep_interval = timeout / 10 + + raw_message: PubSubMessage | None = None + + while True: + with anyio.move_on_after(timeout): + while ( + raw_message := await self._get_message(self.subscription) + ) is None: + await anyio.sleep(sleep_interval) + + context = self._outer_config.fd_config.context + + if raw_message is None: + continue + + msg: RedisMessage = await process_msg( # type: ignore[assignment] + msg=raw_message, + middlewares=( + m(raw_message, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + yield msg + + async def _get_message(self, psub: RPubSub) -> PubSubMessage | None: + raw_msg = await psub.get_message( + ignore_subscribe_messages=True, + timeout=self.channel.polling_interval, + ) + + if raw_msg: + return PubSubMessage( + type=raw_msg["type"], + data=raw_msg["data"], + channel=raw_msg["channel"].decode(), + pattern=raw_msg["pattern"], + ) + + return None + + async def _get_msgs(self, psub: RPubSub) -> None: + if msg := await self._get_message(psub): + await self.consume_one(msg) + + +class ChannelConcurrentSubscriber( + ConcurrentMixin["BrokerStreamMessage"], + ChannelSubscriber, +): + async def start(self) -> None: + await super().start() + self.start_consume_task() + + async def consume_one(self, msg: "BrokerStreamMessage") -> None: + await self._put_msg(msg) diff --git a/faststream/redis/subscriber/usecases/list_subscriber.py b/faststream/redis/subscriber/usecases/list_subscriber.py new file mode 100644 index 0000000000..0452a11991 --- /dev/null +++ b/faststream/redis/subscriber/usecases/list_subscriber.py @@ -0,0 +1,219 @@ +from collections.abc import AsyncIterator +from typing import ( + TYPE_CHECKING, + Any, + Optional, + TypeAlias, +) + +import anyio +from typing_extensions import override + +from faststream._internal.endpoint.subscriber.mixins import ConcurrentMixin +from faststream._internal.endpoint.utils import process_msg +from faststream.redis.message import ( + BatchListMessage, + DefaultListMessage, + RedisListMessage, +) +from faststream.redis.parser import ( + RedisBatchListParser, + RedisListParser, +) + +from .basic import LogicSubscriber + +if TYPE_CHECKING: + from redis.asyncio.client import Redis + + from faststream._internal.endpoint.subscriber.call_item import ( + CallsCollection, + ) + from faststream.message import StreamMessage as BrokerStreamMessage + from faststream.redis.schemas import ListSub + from faststream.redis.subscriber.config import RedisSubscriberConfig + from faststream.redis.subscriber.specification import RedisSubscriberSpecification + +TopicName: TypeAlias = bytes +Offset: TypeAlias = bytes + + +class _ListHandlerMixin(LogicSubscriber): + def __init__(self, config: "RedisSubscriberConfig", specification: "RedisSubscriberSpecification", calls: "CallsCollection[Any]") -> None: + super().__init__(config, specification, calls) + assert config.list_sub # nosec B101 + self._list_sub = config.list_sub + + @property + def list_sub(self) -> "ListSub": + return self._list_sub.add_prefix(self._outer_config.prefix) + + def get_log_context( + self, + message: Optional["BrokerStreamMessage[Any]"], + ) -> dict[str, str]: + return self.build_log_context( + message=message, + channel=self.list_sub.name, + ) + + @override + async def _consume( # type: ignore[override] + self, + client: "Redis[bytes]", + *, + start_signal: "anyio.Event", + ) -> None: + if await client.ping(): + start_signal.set() + await super()._consume(client, start_signal=start_signal) + + @override + async def start(self) -> None: + if self.tasks: + return + + assert self._client, "You should setup subscriber at first." # nosec B101 + + await super().start(self._client) + + @override + async def get_one( + self, + *, + timeout: float = 5.0, + ) -> "RedisListMessage | None": + assert self._client, "You should start subscriber at first." # nosec B101 + assert ( # nosec B101 + not self.calls + ), "You can't use `get_one` method if subscriber has registered handlers." + + sleep_interval = timeout / 10 + raw_message = None + + with anyio.move_on_after(timeout): + while ( # noqa: ASYNC110 + raw_message := await self._client.lpop(name=self.list_sub.name) + ) is None: + await anyio.sleep(sleep_interval) + + if not raw_message: + return None + + redis_incoming_msg = DefaultListMessage( + type="list", + data=raw_message, + channel=self.list_sub.name, + ) + + context = self._outer_config.fd_config.context + + msg: RedisListMessage = await process_msg( # type: ignore[assignment] + msg=redis_incoming_msg, + middlewares=( + m(redis_incoming_msg, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + return msg + + @override + async def __aiter__(self) -> AsyncIterator["RedisListMessage"]: # type: ignore[override] + assert self._client, "You should start subscriber at first." # nosec B101 + assert ( # nosec B101 + not self.calls + ), "You can't use iterator if subscriber has registered handlers." + + timeout = 5 + sleep_interval = timeout / 10 + raw_message = None + + while True: + with anyio.move_on_after(timeout): + while ( # noqa: ASYNC110 + raw_message := await self._client.lpop(name=self.list_sub.name) + ) is None: + await anyio.sleep(sleep_interval) + + if not raw_message: + continue + + redis_incoming_msg = DefaultListMessage( + type="list", + data=raw_message, + channel=self.list_sub.name, + ) + + context = self._outer_config.fd_config.context + + msg: RedisListMessage = await process_msg( # type: ignore[assignment] + msg=redis_incoming_msg, + middlewares=( + m(redis_incoming_msg, context=context) + for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + yield msg + + +class ListSubscriber(_ListHandlerMixin): + def __init__(self, config: "RedisSubscriberConfig", specification: "RedisSubscriberSpecification", calls: "CallsCollection[Any]") -> None: + parser = RedisListParser() + config.parser = parser.parse_message + config.decoder = parser.decode_message + super().__init__(config, specification, calls) + + async def _get_msgs(self, client: "Redis[bytes]") -> None: + raw_msg = await client.blpop( + self.list_sub.name, + timeout=self.list_sub.polling_interval, + ) + + if raw_msg: + _, msg_data = raw_msg + + msg = DefaultListMessage( + type="list", + data=msg_data, + channel=self.list_sub.name, + ) + + await self.consume_one(msg) + + +class ListBatchSubscriber(_ListHandlerMixin): + def __init__(self, config: "RedisSubscriberConfig", specification: "RedisSubscriberSpecification", calls: "CallsCollection[Any]") -> None: + parser = RedisBatchListParser() + config.parser = parser.parse_message + config.decoder = parser.decode_message + super().__init__(config, specification, calls) + + async def _get_msgs(self, client: "Redis[bytes]") -> None: + raw_msgs = await client.lpop( + name=self.list_sub.name, + count=self.list_sub.max_records, + ) + + if raw_msgs: + msg = BatchListMessage( + type="blist", + channel=self.list_sub.name, + data=raw_msgs, + ) + + await self.consume_one(msg) + + else: + await anyio.sleep(self.list_sub.polling_interval) + + +class ListConcurrentSubscriber(ConcurrentMixin["BrokerStreamMessage"], ListSubscriber): + async def start(self) -> None: + await super().start() + self.start_consume_task() + + async def consume_one(self, msg: "BrokerStreamMessage") -> None: + await self._put_msg(msg) diff --git a/faststream/redis/subscriber/usecases/stream_subscriber.py b/faststream/redis/subscriber/usecases/stream_subscriber.py new file mode 100644 index 0000000000..a3866406c2 --- /dev/null +++ b/faststream/redis/subscriber/usecases/stream_subscriber.py @@ -0,0 +1,345 @@ +import math +from collections.abc import AsyncIterator, Awaitable, Callable +from typing import ( + TYPE_CHECKING, + Any, + Optional, + TypeAlias, +) + +from redis.exceptions import ResponseError +from typing_extensions import override + +from faststream._internal.endpoint.subscriber.mixins import ConcurrentMixin +from faststream._internal.endpoint.utils import process_msg +from faststream.redis.message import ( + BatchStreamMessage, + DefaultStreamMessage, + RedisStreamMessage, +) +from faststream.redis.parser import ( + RedisBatchStreamParser, + RedisStreamParser, +) + +from .basic import LogicSubscriber + +if TYPE_CHECKING: + from anyio import Event + + from faststream._internal.endpoint.subscriber.call_item import ( + CallsCollection, + ) + from faststream.message import StreamMessage as BrokerStreamMessage + from faststream.redis.schemas import StreamSub + from faststream.redis.subscriber.config import RedisSubscriberConfig + from faststream.redis.subscriber.specification import RedisSubscriberSpecification + + +TopicName: TypeAlias = bytes +Offset: TypeAlias = bytes + + +class _StreamHandlerMixin(LogicSubscriber): + def __init__(self, config: "RedisSubscriberConfig", specification: "RedisSubscriberSpecification", calls: "CallsCollection[Any]") -> None: + super().__init__(config, specification, calls) + + assert config.stream_sub # nosec B101 + self._stream_sub = config.stream_sub + self.last_id = config.stream_sub.last_id + + @property + def stream_sub(self) -> "StreamSub": + return self._stream_sub.add_prefix(self._outer_config.prefix) + + def get_log_context( + self, + message: Optional["BrokerStreamMessage[Any]"], + ) -> dict[str, str]: + return self.build_log_context( + message=message, + channel=self.stream_sub.name, + ) + + @override + async def _consume(self, *args: Any, start_signal: "Event") -> None: + assert self._client, "You should setup subscriber at first." # nosec B101 + if await self._client.ping(): + start_signal.set() + await super()._consume(*args, start_signal=start_signal) + + @override + async def start(self) -> None: + if self.tasks: + return + + assert self._client, "You should setup subscriber at first." # nosec B101 + + client = self._client + + self.extra_watcher_options.update( + redis=client, + group=self.stream_sub.group, + ) + + stream = self.stream_sub + + read: Callable[ + [str], + Awaitable[ + tuple[ + tuple[ + TopicName, + tuple[ + tuple[ + Offset, + dict[bytes, bytes], + ], + ..., + ], + ], + ..., + ], + ], + ] + + if stream.group and stream.consumer: + try: + await client.xgroup_create( + name=stream.name, + id=self.last_id, + groupname=stream.group, + mkstream=True, + ) + except ResponseError as e: + if "already exists" not in str(e): + raise + + def read( + _: str, + ) -> Awaitable[ + tuple[ + tuple[ + TopicName, + tuple[ + tuple[ + Offset, + dict[bytes, bytes], + ], + ..., + ], + ], + ..., + ], + ]: + return client.xreadgroup( + groupname=stream.group, + consumername=stream.consumer, + streams={stream.name: ">"}, + count=stream.max_records, + block=stream.polling_interval, + noack=stream.no_ack, + ) + + else: + + def read( + last_id: str, + ) -> Awaitable[ + tuple[ + tuple[ + TopicName, + tuple[ + tuple[ + Offset, + dict[bytes, bytes], + ], + ..., + ], + ], + ..., + ], + ]: + return client.xread( + {stream.name: last_id}, + block=stream.polling_interval, + count=stream.max_records, + ) + + await super().start(read) + + @override + async def get_one( + self, + *, + timeout: float = 5.0, + ) -> "RedisStreamMessage | None": + assert self._client, "You should start subscriber at first." # nosec B101 + assert ( # nosec B101 + not self.calls + ), "You can't use `get_one` method if subscriber has registered handlers." + + stream_message = await self._client.xread( + {self.stream_sub.name: self.last_id}, + block=math.ceil(timeout * 1000), + count=1, + ) + + if not stream_message: + return None + + ((stream_name, ((message_id, raw_message),)),) = stream_message + + self.last_id = message_id.decode() + + redis_incoming_msg = DefaultStreamMessage( + type="stream", + channel=stream_name.decode(), + message_ids=[message_id], + data=raw_message, + ) + + context = self._outer_config.fd_config.context + + msg: RedisStreamMessage = await process_msg( # type: ignore[assignment] + msg=redis_incoming_msg, + middlewares=( + m(redis_incoming_msg, context=context) for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + return msg + + @override + async def __aiter__(self) -> AsyncIterator["RedisStreamMessage"]: + assert self._client, "You should start subscriber at first." # nosec B101 + assert ( # nosec B101 + not self.calls + ), "You can't use iterator if subscriber has registered handlers." + + timeout = 5 + while True: + stream_message = await self._client.xread( + {self.stream_sub.name: self.last_id}, + block=math.ceil(timeout * 1000), + count=1, + ) + + if not stream_message: + continue + + ((stream_name, ((message_id, raw_message),)),) = stream_message + + self.last_id = message_id.decode() + + redis_incoming_msg = DefaultStreamMessage( + type="stream", + channel=stream_name.decode(), + message_ids=[message_id], + data=raw_message, + ) + + context = self._outer_config.fd_config.context + + msg: RedisStreamMessage = await process_msg( # type: ignore[assignment] + msg=redis_incoming_msg, + middlewares=( + m(redis_incoming_msg, context=context) + for m in self._broker_middlewares + ), + parser=self._parser, + decoder=self._decoder, + ) + yield msg + + +class StreamSubscriber(_StreamHandlerMixin): + def __init__(self, config: "RedisSubscriberConfig", specification: "RedisSubscriberSpecification", calls: "CallsCollection[Any]") -> None: + parser = RedisStreamParser() + config.decoder = parser.decode_message + config.parser = parser.parse_message + super().__init__(config, specification, calls) + + async def _get_msgs( + self, + read: Callable[ + [str], + Awaitable[ + tuple[ + tuple[ + TopicName, + tuple[ + tuple[ + Offset, + dict[bytes, bytes], + ], + ..., + ], + ], + ..., + ], + ], + ], + ) -> None: + for stream_name, msgs in await read(self.last_id): + if msgs: + self.last_id = msgs[-1][0].decode() + + for message_id, raw_msg in msgs: + msg = DefaultStreamMessage( + type="stream", + channel=stream_name.decode(), + message_ids=[message_id], + data=raw_msg, + ) + + await self.consume_one(msg) + + +class StreamBatchSubscriber(_StreamHandlerMixin): + def __init__(self, config: "RedisSubscriberConfig", specification: "RedisSubscriberSpecification", calls: "CallsCollection[Any]") -> None: + parser = RedisBatchStreamParser() + config.decoder = parser.decode_message + config.parser = parser.parse_message + super().__init__(config, specification, calls) + + async def _get_msgs( + self, + read: Callable[ + [str], + Awaitable[ + tuple[tuple[bytes, tuple[tuple[bytes, dict[bytes, bytes]], ...]], ...], + ], + ], + ) -> None: + for stream_name, msgs in await read(self.last_id): + if msgs: + self.last_id = msgs[-1][0].decode() + + data: list[dict[bytes, bytes]] = [] + ids: list[bytes] = [] + for message_id, i in msgs: + data.append(i) + ids.append(message_id) + + msg = BatchStreamMessage( + type="bstream", + channel=stream_name.decode(), + data=data, + message_ids=ids, + ) + + await self.consume_one(msg) + + +class StreamConcurrentSubscriber( + ConcurrentMixin["BrokerStreamMessage"], + StreamSubscriber, +): + async def start(self) -> None: + await super().start() + self.start_consume_task() + + async def consume_one(self, msg: "BrokerStreamMessage") -> None: + await self._put_msg(msg) diff --git a/faststream/redis/testing.py b/faststream/redis/testing.py index 6847ce1055..cc60d3bc6c 100644 --- a/faststream/redis/testing.py +++ b/faststream/redis/testing.py @@ -1,12 +1,11 @@ import re +from collections.abc import Iterator, Sequence +from contextlib import ExitStack, contextmanager from typing import ( TYPE_CHECKING, Any, - List, Optional, Protocol, - Sequence, - Tuple, Union, cast, ) @@ -15,9 +14,10 @@ import anyio from typing_extensions import TypedDict, override -from faststream.broker.message import gen_cor_id -from faststream.broker.utils import resolve_custom_func -from faststream.exceptions import WRONG_PUBLISH_ARGS, SetupError, SubscriberNotFound +from faststream._internal.endpoint.utils import resolve_custom_func +from faststream._internal.testing.broker import TestBroker, change_producer +from faststream.exceptions import SetupError, SubscriberNotFound +from faststream.message import gen_cor_id from faststream.redis.broker.broker import RedisBroker from faststream.redis.message import ( BatchListMessage, @@ -29,21 +29,18 @@ ) from faststream.redis.parser import RawMessage, RedisPubSubParser from faststream.redis.publisher.producer import RedisFastProducer +from faststream.redis.response import DestinationType, RedisPublishCommand from faststream.redis.schemas import INCORRECT_SETUP_MSG -from faststream.redis.subscriber.usecase import ( - ChannelSubscriber, - LogicSubscriber, - _ListHandlerMixin, - _StreamHandlerMixin, -) -from faststream.testing.broker import TestBroker -from faststream.utils.functions import timeout_scope +from faststream.redis.subscriber.usecases.channel_subscriber import ChannelSubscriber +from faststream.redis.subscriber.usecases.list_subscriber import _ListHandlerMixin +from faststream.redis.subscriber.usecases.stream_subscriber import _StreamHandlerMixin if TYPE_CHECKING: - from redis.asyncio.client import Pipeline + from fast_depends.library.serializer import SerializerProto - from faststream.redis.publisher.asyncapi import AsyncAPIPublisher - from faststream.types import AnyDict, SendableMessage + from faststream._internal.basic_types import AnyDict, SendableMessage + from faststream.redis.publisher.usecase import LogicPublisher + from faststream.redis.subscriber.usecases.basic import LogicSubscriber __all__ = ("TestRedisBroker",) @@ -51,17 +48,27 @@ class TestRedisBroker(TestBroker[RedisBroker]): """A class to test Redis brokers.""" + @contextmanager + def _patch_producer(self, broker: RedisBroker) -> Iterator[None]: + fake_producer = FakeProducer(broker) + + with ExitStack() as es: + es.enter_context( + change_producer(broker.config.broker_config, fake_producer) + ) + yield + @staticmethod def create_publisher_fake_subscriber( broker: RedisBroker, - publisher: "AsyncAPIPublisher", - ) -> Tuple["LogicSubscriber", bool]: - sub: Optional[LogicSubscriber] = None + publisher: "LogicPublisher", + ) -> tuple["LogicSubscriber", bool]: + sub: LogicSubscriber | None = None named_property = publisher.subscriber_property(name_only=True) visitors = (ChannelVisitor(), ListVisitor(), StreamVisitor()) - for handler in broker._subscribers.values(): # pragma: no branch + for handler in broker.subscribers: # pragma: no branch for visitor in visitors: if visitor.visit(**named_property, sub=handler): sub = handler @@ -82,17 +89,17 @@ async def _fake_connect( # type: ignore[override] *args: Any, **kwargs: Any, ) -> AsyncMock: - broker._producer = FakeProducer(broker) connection = MagicMock() pub_sub = AsyncMock() async def get_msg(*args: Any, timeout: float, **kwargs: Any) -> None: await anyio.sleep(timeout) - return None pub_sub.get_message = get_msg + broker.config.broker_config.connection._client = connection + connection.pubsub.side_effect = lambda: pub_sub return connection @@ -114,36 +121,20 @@ def __init__(self, broker: RedisBroker) -> None: @override async def publish( self, - message: "SendableMessage", - *, - channel: Optional[str] = None, - list: Optional[str] = None, - stream: Optional[str] = None, - maxlen: Optional[int] = None, - headers: Optional["AnyDict"] = None, - reply_to: str = "", - correlation_id: Optional[str] = None, - rpc: bool = False, - rpc_timeout: Optional[float] = 30.0, - raise_timeout: bool = False, - pipeline: Optional["Pipeline[bytes]"] = None, - ) -> Optional[Any]: - if rpc and reply_to: - raise WRONG_PUBLISH_ARGS - - correlation_id = correlation_id or gen_cor_id() - + cmd: "RedisPublishCommand", + ) -> None: body = build_message( - message=message, - reply_to=reply_to, - correlation_id=correlation_id, - headers=headers, + message=cmd.body, + reply_to=cmd.reply_to, + correlation_id=cmd.correlation_id or gen_cor_id(), + headers=cmd.headers, + serializer=self.broker.config.fd_config._serializer ) - destination = _make_destionation_kwargs(channel, list, stream) + destination = _make_destionation_kwargs(cmd) visitors = (ChannelVisitor(), ListVisitor(), StreamVisitor()) - for handler in self.broker._subscribers.values(): # pragma: no branch + for handler in self.broker.subscribers: # pragma: no branch for visitor in visitors: if visited_ch := visitor.visit(**destination, sub=handler): msg = visitor.get_message( @@ -152,38 +143,23 @@ async def publish( handler, # type: ignore[arg-type] ) - with timeout_scope(rpc_timeout, raise_timeout): - response_msg = await self._execute_handler(msg, handler) - if rpc: - return await self._decoder(await self._parser(response_msg)) - - return None + await self._execute_handler(msg, handler) @override async def request( # type: ignore[override] self, - message: "SendableMessage", - *, - correlation_id: str, - channel: Optional[str] = None, - list: Optional[str] = None, - stream: Optional[str] = None, - maxlen: Optional[int] = None, - headers: Optional["AnyDict"] = None, - timeout: Optional[float] = 30.0, + cmd: "RedisPublishCommand", ) -> "PubSubMessage": - correlation_id = correlation_id or gen_cor_id() - body = build_message( - message=message, - correlation_id=correlation_id, - headers=headers, + message=cmd.body, + correlation_id=cmd.correlation_id or gen_cor_id(), + headers=cmd.headers, ) - destination = _make_destionation_kwargs(channel, list, stream) + destination = _make_destionation_kwargs(cmd) visitors = (ChannelVisitor(), ListVisitor(), StreamVisitor()) - for handler in self.broker._subscribers.values(): # pragma: no branch + for handler in self.broker.subscribers: # pragma: no branch for visitor in visitors: if visited_ch := visitor.visit(**destination, sub=handler): msg = visitor.get_message( @@ -192,42 +168,40 @@ async def request( # type: ignore[override] handler, # type: ignore[arg-type] ) - with anyio.fail_after(timeout): + with anyio.fail_after(cmd.timeout): return await self._execute_handler(msg, handler) raise SubscriberNotFound async def publish_batch( self, - *msgs: "SendableMessage", - list: str, - headers: Optional["AnyDict"] = None, - correlation_id: Optional[str] = None, - pipeline: Optional["Pipeline[bytes]"] = None, + cmd: "RedisPublishCommand", ) -> None: data_to_send = [ build_message( m, - correlation_id=correlation_id or gen_cor_id(), - headers=headers, + correlation_id=cmd.correlation_id or gen_cor_id(), + headers=cmd.headers, ) - for m in msgs + for m in cmd.batch_bodies ] visitor = ListVisitor() - for handler in self.broker._subscribers.values(): # pragma: no branch - if visitor.visit(list=list, sub=handler): + for handler in self.broker.subscribers: # pragma: no branch) + if visitor.visit(list=cmd.destination, sub=handler): casted_handler = cast("_ListHandlerMixin", handler) if casted_handler.list_sub.batch: - msg = visitor.get_message(list, data_to_send, casted_handler) + msg = visitor.get_message( + cmd.destination, data_to_send, casted_handler + ) await self._execute_handler(msg, handler) - return None - async def _execute_handler( - self, msg: Any, handler: "LogicSubscriber" + self, + msg: Any, + handler: "LogicSubscriber", ) -> "PubSubMessage": result = await handler.process_message(msg) @@ -237,6 +211,7 @@ async def _execute_handler( message=result.body, headers=result.headers, correlation_id=result.correlation_id or "", + serializer=self.broker.config.fd_config._serializer ), channel="", pattern=None, @@ -249,25 +224,26 @@ def build_message( correlation_id: str, reply_to: str = "", headers: Optional["AnyDict"] = None, + serializer: Optional["SerializerProto"] = None ) -> bytes: - data = RawMessage.encode( + return RawMessage.encode( message=message, reply_to=reply_to, headers=headers, correlation_id=correlation_id, + serializer=serializer ) - return data class Visitor(Protocol): def visit( self, *, - channel: Optional[str], - list: Optional[str], - stream: Optional[str], + channel: str | None, + list: str | None, + stream: str | None, sub: "LogicSubscriber", - ) -> Optional[str]: ... + ) -> str | None: ... def get_message(self, channel: str, body: Any, sub: "LogicSubscriber") -> Any: ... @@ -277,10 +253,10 @@ def visit( self, *, sub: "LogicSubscriber", - channel: Optional[str] = None, - list: Optional[str] = None, - stream: Optional[str] = None, - ) -> Optional[str]: + channel: str | None = None, + list: str | None = None, + stream: str | None = None, + ) -> str | None: if channel is None or not isinstance(sub, ChannelSubscriber): return None @@ -292,7 +268,7 @@ def visit( re.match( sub_channel.name.replace(".", "\\.").replace("*", ".*"), channel or "", - ) + ), ) ) or channel == sub_channel.name: return channel @@ -318,10 +294,10 @@ def visit( self, *, sub: "LogicSubscriber", - channel: Optional[str] = None, - list: Optional[str] = None, - stream: Optional[str] = None, - ) -> Optional[str]: + channel: str | None = None, + list: str | None = None, + stream: str | None = None, + ) -> str | None: if list is None or not isinstance(sub, _ListHandlerMixin): return None @@ -340,15 +316,14 @@ def get_message( # type: ignore[override] return BatchListMessage( type="blist", channel=channel, - data=body if isinstance(body, List) else [body], + data=body if isinstance(body, list) else [body], ) - else: - return DefaultListMessage( - type="list", - channel=channel, - data=body, - ) + return DefaultListMessage( + type="list", + channel=channel, + data=body, + ) class StreamVisitor(Visitor): @@ -356,10 +331,10 @@ def visit( self, *, sub: "LogicSubscriber", - channel: Optional[str] = None, - list: Optional[str] = None, - stream: Optional[str] = None, - ) -> Optional[str]: + channel: str | None = None, + list: str | None = None, + stream: str | None = None, + ) -> str | None: if stream is None or not isinstance(sub, _StreamHandlerMixin): return None @@ -382,13 +357,12 @@ def get_message( # type: ignore[override] message_ids=[], ) - else: - return DefaultStreamMessage( - type="stream", - channel=channel, - data={bDATA_KEY: body}, - message_ids=[], - ) + return DefaultStreamMessage( + type="stream", + channel=channel, + data={bDATA_KEY: body}, + message_ids=[], + ) class _DestinationKwargs(TypedDict, total=False): @@ -397,18 +371,14 @@ class _DestinationKwargs(TypedDict, total=False): stream: str -def _make_destionation_kwargs( - channel: Optional[str], - list: Optional[str], - stream: Optional[str], -) -> _DestinationKwargs: +def _make_destionation_kwargs(cmd: RedisPublishCommand) -> _DestinationKwargs: destination: _DestinationKwargs = {} - if channel: - destination["channel"] = channel - if list: - destination["list"] = list - if stream: - destination["stream"] = stream + if cmd.destination_type is DestinationType.Channel: + destination["channel"] = cmd.destination + if cmd.destination_type is DestinationType.List: + destination["list"] = cmd.destination + if cmd.destination_type is DestinationType.Stream: + destination["stream"] = cmd.destination if len(destination) != 1: raise SetupError(INCORRECT_SETUP_MSG) diff --git a/faststream/response/__init__.py b/faststream/response/__init__.py new file mode 100644 index 0000000000..36c071f516 --- /dev/null +++ b/faststream/response/__init__.py @@ -0,0 +1,11 @@ +from .publish_type import PublishType +from .response import BatchPublishCommand, PublishCommand, Response +from .utils import ensure_response + +__all__ = ( + "BatchPublishCommand", + "PublishCommand", + "PublishType", + "Response", + "ensure_response", +) diff --git a/faststream/response/publish_type.py b/faststream/response/publish_type.py new file mode 100644 index 0000000000..ad74910a1e --- /dev/null +++ b/faststream/response/publish_type.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class PublishType(str, Enum): + PUBLISH = "PUBLISH" + """Regular `broker/publisher.publish(...)` call.""" + + REPLY = "REPLY" + """Response to RPC/Reply-To request.""" + + REQUEST = "REQUEST" + """RPC request call.""" diff --git a/faststream/response/response.py b/faststream/response/response.py new file mode 100644 index 0000000000..29841e63cc --- /dev/null +++ b/faststream/response/response.py @@ -0,0 +1,132 @@ +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Optional + +from .publish_type import PublishType + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict + + +class Response: + def __init__( + self, + body: Any, + *, + headers: Optional["AnyDict"] = None, + correlation_id: str | None = None, + ) -> None: + """Initialize a handler.""" + self.body = body + self.headers = headers or {} + self.correlation_id = correlation_id + + def as_publish_command(self) -> "PublishCommand": + """Method to transform handlers' Response result to DTO for publishers.""" + return PublishCommand( + body=self.body, + headers=self.headers, + correlation_id=self.correlation_id, + _publish_type=PublishType.PUBLISH, + ) + + +class PublishCommand(Response): + def __init__( + self, + body: Any, + *, + _publish_type: PublishType, + reply_to: str = "", + destination: str = "", + correlation_id: str | None = None, + headers: Optional["AnyDict"] = None, + ) -> None: + super().__init__( + body, + headers=headers, + correlation_id=correlation_id, + ) + + self.destination = destination + self.reply_to = reply_to + + self.publish_type = _publish_type + + @property + def batch_bodies(self) -> tuple["Any", ...]: + if self.body is not None: + return (self.body,) + return () + + def add_headers( + self, + headers: "AnyDict", + *, + override: bool = True, + ) -> None: + if override: + self.headers |= headers + else: + self.headers = headers | self.headers + + @classmethod + def from_cmd( + cls, + cmd: "PublishCommand", + ) -> "PublishCommand": + raise NotImplementedError + + +class BatchPublishCommand(PublishCommand): + def __init__( + self, + body: Any, + /, + *bodies: Any, + _publish_type: PublishType, + reply_to: str = "", + destination: str = "", + correlation_id: str | None = None, + headers: Optional["AnyDict"] = None, + ) -> None: + super().__init__( + body, + headers=headers, + correlation_id=correlation_id, + destination=destination, + reply_to=reply_to, + _publish_type=_publish_type, + ) + self.extra_bodies = bodies + + @property + def batch_bodies(self) -> tuple["Any", ...]: + return (*super().batch_bodies, *self.extra_bodies) + + @batch_bodies.setter + def batch_bodies(self, value: Sequence["Any"]) -> None: + if len(value) == 0: + self.body = None + self.extra_bodies = () + else: + self.body = value[0] + self.extra_bodies = tuple(value[1:]) + + @classmethod + def from_cmd( + cls, + cmd: "PublishCommand", + *, + batch: bool = False, + ) -> "BatchPublishCommand": + raise NotImplementedError + + @staticmethod + def _parse_bodies(body: Any, *, batch: bool = False) -> tuple[Any, tuple[Any, ...]]: + extra_bodies = [] + if batch and isinstance(body, Sequence) and not isinstance(body, (str, bytes)): + if body: + body, extra_bodies = body[0], body[1:] + else: + body = None + return body, tuple(extra_bodies) diff --git a/faststream/response/utils.py b/faststream/response/utils.py new file mode 100644 index 0000000000..5ddfa604c4 --- /dev/null +++ b/faststream/response/utils.py @@ -0,0 +1,10 @@ +from typing import Any + +from .response import Response + + +def ensure_response(response: Response | Any) -> Response: + if isinstance(response, Response): + return response + + return Response(response) diff --git a/faststream/security.py b/faststream/security.py index cc693057c6..f8a3c882ea 100644 --- a/faststream/security.py +++ b/faststream/security.py @@ -1,9 +1,9 @@ -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from ssl import SSLContext - from faststream.types import AnyDict + from faststream._internal.basic_types import AnyDict class BaseSecurity: @@ -19,7 +19,7 @@ class BaseSecurity: def __init__( self, ssl_context: Optional["SSLContext"] = None, - use_ssl: Optional[bool] = None, + use_ssl: bool | None = None, ) -> None: if ssl_context is not None: use_ssl = True @@ -30,11 +30,11 @@ def __init__( self.use_ssl = use_ssl self.ssl_context = ssl_context - def get_requirement(self) -> List["AnyDict"]: + def get_requirement(self) -> list["AnyDict"]: """Get the security requirements.""" return [] - def get_schema(self) -> Dict[str, Dict[str, str]]: + def get_schema(self) -> dict[str, dict[str, str]]: """Get the security schema.""" return {} @@ -58,7 +58,7 @@ def __init__( username: str, password: str, ssl_context: Optional["SSLContext"] = None, - use_ssl: Optional[bool] = None, + use_ssl: bool | None = None, ) -> None: super().__init__( ssl_context=ssl_context, @@ -68,11 +68,11 @@ def __init__( self.username = username self.password = password - def get_requirement(self) -> List["AnyDict"]: + def get_requirement(self) -> list["AnyDict"]: """Get the security requirements for SASL/PLAINTEXT authentication.""" return [{"user-password": []}] - def get_schema(self) -> Dict[str, Dict[str, str]]: + def get_schema(self) -> dict[str, dict[str, str]]: """Get the security schema for SASL/PLAINTEXT authentication.""" return {"user-password": {"type": "userPassword"}} @@ -96,7 +96,7 @@ def __init__( username: str, password: str, ssl_context: Optional["SSLContext"] = None, - use_ssl: Optional[bool] = None, + use_ssl: bool | None = None, ) -> None: super().__init__( ssl_context=ssl_context, @@ -106,11 +106,11 @@ def __init__( self.username = username self.password = password - def get_requirement(self) -> List["AnyDict"]: + def get_requirement(self) -> list["AnyDict"]: """Get the security requirements for SASL/SCRAM-SHA-256 authentication.""" return [{"scram256": []}] - def get_schema(self) -> Dict[str, Dict[str, str]]: + def get_schema(self) -> dict[str, dict[str, str]]: """Get the security schema for SASL/SCRAM-SHA-256 authentication.""" return {"scram256": {"type": "scramSha256"}} @@ -134,7 +134,7 @@ def __init__( username: str, password: str, ssl_context: Optional["SSLContext"] = None, - use_ssl: Optional[bool] = None, + use_ssl: bool | None = None, ) -> None: super().__init__( ssl_context=ssl_context, @@ -144,11 +144,11 @@ def __init__( self.username = username self.password = password - def get_requirement(self) -> List["AnyDict"]: + def get_requirement(self) -> list["AnyDict"]: """Get the security requirements for SASL/SCRAM-SHA-512 authentication.""" return [{"scram512": []}] - def get_schema(self) -> Dict[str, Dict[str, str]]: + def get_schema(self) -> dict[str, dict[str, str]]: """Get the security schema for SASL/SCRAM-SHA-512 authentication.""" return {"scram512": {"type": "scramSha512"}} @@ -161,11 +161,11 @@ class SASLOAuthBearer(BaseSecurity): __slots__ = ("ssl_context", "use_ssl") - def get_requirement(self) -> List["AnyDict"]: + def get_requirement(self) -> list["AnyDict"]: """Get the security requirements for SASL/OAUTHBEARER authentication.""" return [{"oauthbearer": []}] - def get_schema(self) -> Dict[str, Dict[str, str]]: + def get_schema(self) -> dict[str, dict[str, str]]: """Get the security schema for SASL/OAUTHBEARER authentication.""" return {"oauthbearer": {"type": "oauth2", "$ref": ""}} @@ -178,10 +178,10 @@ class SASLGSSAPI(BaseSecurity): __slots__ = ("ssl_context", "use_ssl") - def get_requirement(self) -> List["AnyDict"]: + def get_requirement(self) -> list["AnyDict"]: """Get the security requirements for SASL/GSSAPI authentication.""" return [{"gssapi": []}] - def get_schema(self) -> Dict[str, Dict[str, str]]: + def get_schema(self) -> dict[str, dict[str, str]]: """Get the security schema for SASL/GSSAPI authentication.""" return {"gssapi": {"type": "gssapi"}} diff --git a/faststream/specification/__init__.py b/faststream/specification/__init__.py new file mode 100644 index 0000000000..e880c36640 --- /dev/null +++ b/faststream/specification/__init__.py @@ -0,0 +1,12 @@ +from .asyncapi.factory import AsyncAPI +from .base.specification import Specification +from .schema.extra import Contact, ExternalDocs, License, Tag + +__all__ = ( + "AsyncAPI", + "Contact", + "ExternalDocs", + "License", + "Specification", + "Tag", +) diff --git a/faststream/specification/asyncapi/__init__.py b/faststream/specification/asyncapi/__init__.py new file mode 100644 index 0000000000..fe93b5941d --- /dev/null +++ b/faststream/specification/asyncapi/__init__.py @@ -0,0 +1,10 @@ +"""AsyncAPI related functions.""" + +from faststream.specification.asyncapi.site import get_asyncapi_html + +from .factory import AsyncAPI + +__all__ = ( + "AsyncAPI", + "get_asyncapi_html", +) diff --git a/faststream/specification/asyncapi/factory.py b/faststream/specification/asyncapi/factory.py new file mode 100644 index 0000000000..775c171ab1 --- /dev/null +++ b/faststream/specification/asyncapi/factory.py @@ -0,0 +1,66 @@ +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Literal, Optional, Union + +from faststream.specification.base.specification import Specification + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict, AnyHttpUrl + from faststream._internal.broker import BrokerUsecase + from faststream.specification.schema import ( + Contact, + ExternalDocs, + License, + Tag, + ) + + +def AsyncAPI( # noqa: N802 + broker: "BrokerUsecase[Any, Any]", + /, + title: str = "FastStream", + app_version: str = "0.1.0", + schema_version: Literal["3.0.0", "2.6.0"] | str = "3.0.0", + description: str = "", + terms_of_service: Optional["AnyHttpUrl"] = None, + license: Union["License", "AnyDict"] | None = None, + contact: Union["Contact", "AnyDict"] | None = None, + tags: Sequence[Union["Tag", "AnyDict"]] = (), + external_docs: Union["ExternalDocs", "AnyDict"] | None = None, + identifier: str | None = None, +) -> Specification: + if schema_version.startswith("3.0."): + from .v3_0_0.facade import AsyncAPI3 + + return AsyncAPI3( + broker, + title=title, + app_version=app_version, + schema_version=schema_version, + description=description, + terms_of_service=terms_of_service, + contact=contact, + license=license, + identifier=identifier, + tags=tags, + external_docs=external_docs, + ) + + if schema_version.startswith("2.6."): + from .v2_6_0.facade import AsyncAPI2 + + return AsyncAPI2( + broker, + title=title, + app_version=app_version, + schema_version=schema_version, + description=description, + terms_of_service=terms_of_service, + contact=contact, + license=license, + identifier=identifier, + tags=tags, + external_docs=external_docs, + ) + + msg = f"Unsupported schema version: {schema_version}" + raise NotImplementedError(msg) diff --git a/faststream/specification/asyncapi/message.py b/faststream/specification/asyncapi/message.py new file mode 100644 index 0000000000..c512032417 --- /dev/null +++ b/faststream/specification/asyncapi/message.py @@ -0,0 +1,142 @@ +from collections.abc import Sequence +from inspect import isclass +from typing import TYPE_CHECKING, Optional, overload + +from pydantic import BaseModel, create_model + +from faststream._internal._compat import ( + DEF_KEY, + PYDANTIC_V2, + get_model_fields, + model_schema, +) +from faststream._internal.basic_types import AnyDict + +if TYPE_CHECKING: + from fast_depends.core import CallModel + + +def parse_handler_params(call: "CallModel", prefix: str = "") -> AnyDict: + """Parses the handler parameters.""" + model = getattr(call, "serializer", call).model + assert model # nosec B101 + + body = get_model_schema( + create_model( + model.__name__, + **{p.field_name: (p.field_type, p.default_value) for p in call.flat_params}, + ), + prefix=prefix, + exclude=tuple(call.custom_fields.keys()), + ) + + if body is None: + return {"title": "EmptyPayload", "type": "null"} + + return body + + +@overload +def get_response_schema(call: None, prefix: str = "") -> None: ... + + +@overload +def get_response_schema(call: "CallModel", prefix: str = "") -> AnyDict: ... + + +def get_response_schema( + call: Optional["CallModel"], + prefix: str = "", +) -> AnyDict | None: + """Get the response schema for a given call.""" + return get_model_schema( + getattr( + call, + "response_model", + None, + ), # NOTE: FastAPI Dependant object compatibility + prefix=prefix, + ) + + +@overload +def get_model_schema( + call: None, + prefix: str = "", + exclude: Sequence[str] = (), +) -> None: ... + + +@overload +def get_model_schema( + call: type[BaseModel], + prefix: str = "", + exclude: Sequence[str] = (), +) -> AnyDict: ... + + +def get_model_schema( + call: type[BaseModel] | None, + prefix: str = "", + exclude: Sequence[str] = (), +) -> AnyDict | None: + """Get the schema of a model.""" + if call is None: + return None + + params = {k: v for k, v in get_model_fields(call).items() if k not in exclude} + params_number = len(params) + + if params_number == 0: + return None + + model = None + use_original_model = False + if params_number == 1: + name, param = next(iter(params.items())) + if ( + param.annotation + and isclass(param.annotation) + and issubclass(param.annotation, BaseModel) # NOTE: 3.7-3.10 compatibility + ): + model = param.annotation + use_original_model = True + + if model is None: + model = call + + body: AnyDict = model_schema(model) + body["properties"] = body.get("properties", {}) + for i in exclude: + body["properties"].pop(i, None) + if required := body.get("required"): + body["required"] = list(filter(lambda x: x not in exclude, required)) + + if params_number == 1 and not use_original_model: + param_body: AnyDict = body.get("properties", {}) + param_body = param_body[name] + + if defs := body.get(DEF_KEY): + # single argument with useless reference + if param_body.get("$ref"): + ref_obj: AnyDict = next(iter(defs.values())) + ref_obj[DEF_KEY] = { + k: v for k, v in defs.items() if k != ref_obj.get("title") + } + return ref_obj + param_body[DEF_KEY] = defs + + original_title = param.title if PYDANTIC_V2 else param.field_info.title + + if original_title: + use_original_model = True + param_body["title"] = original_title + else: + param_body["title"] = name + + body = param_body + + if not use_original_model: + body["title"] = f"{prefix}:Payload" + + return body diff --git a/faststream/asyncapi/site.py b/faststream/specification/asyncapi/site.py similarity index 87% rename from faststream/asyncapi/site.py rename to faststream/specification/asyncapi/site.py index 8cc837c69e..de2a5ca3f5 100644 --- a/faststream/asyncapi/site.py +++ b/faststream/specification/asyncapi/site.py @@ -1,24 +1,23 @@ from functools import partial from http import server -from typing import TYPE_CHECKING, Any, Dict +from typing import TYPE_CHECKING, Any from urllib.parse import parse_qs, urlparse -from faststream._compat import json_dumps -from faststream.log import logger +from faststream._internal._compat import json_dumps +from faststream._internal.logger import logger if TYPE_CHECKING: - from faststream.asyncapi.schema import Schema + from faststream.specification.base.schema import BaseApplicationSchema - -ASYNCAPI_JS_DEFAULT_URL = "https://unpkg.com/@asyncapi/react-component@1.0.0-next.47/browser/standalone/index.js" +ASYNCAPI_JS_DEFAULT_URL = "https://unpkg.com/@asyncapi/react-component@1.0.0-next.54/browser/standalone/index.js" ASYNCAPI_CSS_DEFAULT_URL = ( - "https://unpkg.com/@asyncapi/react-component@1.0.0-next.46/styles/default.min.css" + "https://unpkg.com/@asyncapi/react-component@1.0.0-next.54/styles/default.min.css" ) def get_asyncapi_html( - schema: "Schema", + schema: "BaseApplicationSchema", sidebar: bool = True, info: bool = True, servers: bool = True, @@ -27,7 +26,6 @@ def get_asyncapi_html( schemas: bool = True, errors: bool = True, expand_message_examples: bool = True, - title: str = "FastStream", asyncapi_js_url: str = ASYNCAPI_JS_DEFAULT_URL, asyncapi_css_url: str = ASYNCAPI_CSS_DEFAULT_URL, ) -> str: @@ -63,7 +61,7 @@ def get_asyncapi_html( """ f""" - {title} AsyncAPI + {schema.info.title} AsyncAPI """ """ @@ -103,7 +101,7 @@ def get_asyncapi_html( def serve_app( - schema: "Schema", + schema: "BaseApplicationSchema", host: str, port: int, ) -> None: @@ -121,13 +119,13 @@ class _Handler(server.BaseHTTPRequestHandler): def __init__( self, *args: Any, - schema: "Schema", + schema: "BaseApplicationSchema", **kwargs: Any, ) -> None: self.schema = schema super().__init__(*args, **kwargs) - def get_query_params(self) -> Dict[str, bool]: + def get_query_params(self) -> dict[str, bool]: return { i: _str_to_bool(next(iter(j))) if j else False for i, j in parse_qs(urlparse(self.path).query).items() @@ -148,7 +146,6 @@ def do_GET(self) -> None: # noqa: N802 schemas=query_dict.get("schemas", True), errors=query_dict.get("errors", True), expand_message_examples=query_dict.get("expandMessageExamples", True), - title=self.schema.info.title, ) body = html.encode(encoding=encoding) @@ -160,4 +157,4 @@ def do_GET(self) -> None: # noqa: N802 def _str_to_bool(v: str) -> bool: - return v.lower() in ("1", "t", "true", "y", "yes") + return v.lower() in {"1", "t", "true", "y", "yes"} diff --git a/faststream/specification/asyncapi/utils.py b/faststream/specification/asyncapi/utils.py new file mode 100644 index 0000000000..7f16a215dc --- /dev/null +++ b/faststream/specification/asyncapi/utils.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict + + +def to_camelcase(*names: str) -> str: + return " ".join(names).replace("_", " ").title().replace(" ", "") + + +def resolve_payloads( + payloads: list[tuple["AnyDict", str]], + extra: str = "", + served_words: int = 1, +) -> "AnyDict": + ln = len(payloads) + payload: AnyDict + if ln > 1: + one_of_payloads = {} + + for body, handler_name in payloads: + title = body["title"] + words = title.split(":") + + if len(words) > 1: # not pydantic model case + body["title"] = title = ":".join( + filter( + bool, + ( + handler_name, + extra if extra not in words else "", + *words[served_words:], + ), + ), + ) + + one_of_payloads[title] = body + + payload = {"oneOf": one_of_payloads} + + elif ln == 1: + payload = payloads[0][0] + + else: + payload = {} + + return payload + + +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/__init__.py b/faststream/specification/asyncapi/v2_6_0/__init__.py new file mode 100644 index 0000000000..dd1af249b3 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/__init__.py @@ -0,0 +1,7 @@ +from .facade import AsyncAPI2 +from .generate import get_app_schema + +__all__ = ( + "AsyncAPI2", + "get_app_schema", +) diff --git a/faststream/specification/asyncapi/v2_6_0/facade.py b/faststream/specification/asyncapi/v2_6_0/facade.py new file mode 100644 index 0000000000..2824c4db03 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/facade.py @@ -0,0 +1,75 @@ +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Optional, Union + +from faststream.specification.base.specification import Specification + +from .generate import get_app_schema +from .schema import ApplicationSchema + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict, AnyHttpUrl + from faststream._internal.broker import BrokerUsecase + from faststream.specification.schema import ( + Contact, + ContactDict, + ExternalDocs, + ExternalDocsDict, + License, + LicenseDict, + Tag, + TagDict, + ) + + +class AsyncAPI2(Specification): + def __init__( + self, + broker: "BrokerUsecase[Any, Any]", + /, + title: str = "FastStream", + app_version: str = "0.1.0", + schema_version: str = "3.0.0", + description: str = "", + terms_of_service: Optional["AnyHttpUrl"] = None, + contact: Union["Contact", "ContactDict", "AnyDict"] | None = None, + license: Union["License", "LicenseDict", "AnyDict"] | None = None, + identifier: str | None = None, + tags: Sequence[Union["Tag", "TagDict", "AnyDict"]] = (), + external_docs: Union["ExternalDocs", "ExternalDocsDict", "AnyDict"] | None = None, + ) -> None: + self.broker = broker + self.title = title + self.app_version = app_version + self.schema_version = schema_version + self.description = description + self.terms_of_service = terms_of_service + self.contact = contact + self.license = license + self.identifier = identifier + self.tags = tags + self.external_docs = external_docs + + def to_json(self) -> str: + return self.schema.to_json() + + def to_jsonable(self) -> Any: + return self.schema.to_jsonable() + + def to_yaml(self) -> str: + return self.schema.to_yaml() + + @property + def schema(self) -> ApplicationSchema: + return get_app_schema( + self.broker, + title=self.title, + app_version=self.app_version, + schema_version=self.schema_version, + description=self.description, + terms_of_service=self.terms_of_service, + contact=self.contact, + license=self.license, + identifier=self.identifier, + tags=self.tags, + external_docs=self.external_docs, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/generate.py b/faststream/specification/asyncapi/v2_6_0/generate.py new file mode 100644 index 0000000000..196e305f84 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/generate.py @@ -0,0 +1,235 @@ +import warnings +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Optional, Union + +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, move_pydantic_refs +from faststream.specification.asyncapi.v2_6_0.schema import ( + ApplicationInfo, + ApplicationSchema, + Channel, + Components, + Contact, + ExternalDocs, + License, + Message, + Reference, + Server, + Tag, +) + +if TYPE_CHECKING: + from faststream._internal.broker import BrokerUsecase + from faststream._internal.types import ConnectionType, MsgType + from faststream.specification.schema.extra import ( + Contact as SpecContact, + ContactDict, + ExternalDocs as SpecDocs, + ExternalDocsDict, + License as SpecLicense, + LicenseDict, + Tag as SpecTag, + TagDict, + ) + + +def get_app_schema( + broker: "BrokerUsecase[Any, Any]", + /, + title: str, + app_version: str, + schema_version: str, + description: str, + terms_of_service: Optional["AnyHttpUrl"], + contact: Union["SpecContact", "ContactDict", "AnyDict"] | None, + license: Union["SpecLicense", "LicenseDict", "AnyDict"] | None, + identifier: str | None, + tags: Sequence[Union["SpecTag", "TagDict", "AnyDict"]], + external_docs: Union["SpecDocs", "ExternalDocsDict", "AnyDict"] | None, +) -> ApplicationSchema: + """Get the application schema.""" + servers = get_broker_server(broker) + channels = get_broker_channels(broker) + + messages: dict[str, Message] = {} + payloads: dict[str, AnyDict] = {} + + for channel in channels.values(): + channel.servers = list(servers.keys()) + + for channel_name, ch in channels.items(): + resolve_channel_messages(ch, channel_name, payloads, messages) + + return ApplicationSchema( + info=ApplicationInfo( + title=title, + version=app_version, + description=description, + termsOfService=terms_of_service, + 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, + servers=servers, + channels=channels, + components=Components( + messages=messages, + schemas=payloads, + securitySchemes=None + if broker.specification.security is None + else broker.specification.security.get_schema(), + ), + ) + + +def resolve_channel_messages( + channel: Channel, + channel_name: str, + payloads: dict[str, AnyDict], + messages: dict[str, Message], +) -> None: + if channel.subscribe is not None: + assert isinstance(channel.subscribe.message, Message) + + channel.subscribe.message = _resolve_msg_payloads( + channel.subscribe.message, + channel_name, + payloads, + messages, + ) + + if channel.publish is not None: + assert isinstance(channel.publish.message, Message) + + channel.publish.message = _resolve_msg_payloads( + channel.publish.message, + channel_name, + payloads, + messages, + ) + + +def get_broker_server( + broker: "BrokerUsecase[MsgType, ConnectionType]", +) -> dict[str, Server]: + """Get the broker server for an application.""" + specification = broker.specification + + servers = {} + + broker_meta: AnyDict = { + "protocol": specification.protocol, + "protocolVersion": specification.protocol_version, + "description": specification.description, + "tags": [Tag.from_spec(tag) for tag in specification.tags] or None, + "security": specification.security.get_requirement() + if specification.security + else None, + # TODO + # "variables": "", + # "bindings": "", + } + + single_server = len(specification.url) == 1 + for i, url in enumerate(specification.url, 1): + server_name = "development" if single_server else f"Server{i}" + servers[server_name] = Server(url=url, **broker_meta) + + return servers + + +def get_broker_channels( + broker: "BrokerUsecase[MsgType, ConnectionType]", +) -> dict[str, Channel]: + """Get the broker channels for an application.""" + channels = {} + + for s in filter(lambda s: s.specification.include_in_schema, broker.subscribers): + for key, sub in s.schema().items(): + if key in channels: + warnings.warn( + f"Overwrite channel handler, channels have the same names: `{key}`", + RuntimeWarning, + stacklevel=1, + ) + + channels[key] = Channel.from_sub(sub) + + for p in filter(lambda p: p.specification.include_in_schema, broker.publishers): + for key, pub in p.schema().items(): + if key in channels: + warnings.warn( + f"Overwrite channel handler, channels have the same names: `{key}`", + RuntimeWarning, + stacklevel=1, + ) + + channels[key] = Channel.from_pub(pub) + + return channels + + +def _resolve_msg_payloads( + m: Message, + channel_name: str, + payloads: AnyDict, + messages: AnyDict, +) -> Reference: + """Replace message payload by reference and normalize payloads. + + Payloads and messages are editable dicts to store schemas for reference in AsyncAPI. + """ + one_of_list: list[Reference] = [] + m.payload = move_pydantic_refs(m.payload, DEF_KEY) + + if DEF_KEY in m.payload: + payloads.update(m.payload.pop(DEF_KEY)) + + one_of = m.payload.get("oneOf") + if isinstance(one_of, dict): + for p_title, p in one_of.items(): + formatted_payload_title = clear_key(p_title) + payloads.update(p.pop(DEF_KEY, {})) + if formatted_payload_title not in payloads: + payloads[formatted_payload_title] = p + one_of_list.append( + Reference(**{"$ref": f"#/components/schemas/{formatted_payload_title}"}) + ) + + elif one_of is not None: + # Descriminator case + for p in one_of: + p_value = next(iter(p.values())) + p_title = p_value.split("/")[-1] + p_title = clear_key(p_title) + if p_title not in payloads: + payloads[p_title] = p + one_of_list.append(Reference(**{"$ref": f"#/components/schemas/{p_title}"})) + + if not one_of_list: + payloads.update(m.payload.pop(DEF_KEY, {})) + p_title = m.payload.get("title", f"{channel_name}Payload") + p_title = clear_key(p_title) + if p_title in payloads and payloads[p_title] != m.payload: + warnings.warn( + f"Overwriting the message schema, data types have the same name: `{p_title}`", + RuntimeWarning, + stacklevel=1, + ) + + payloads[p_title] = m.payload + m.payload = {"$ref": f"#/components/schemas/{p_title}"} + + else: + m.payload["oneOf"] = one_of_list + + assert m.title # nosec B101 + message_title = clear_key(m.title) + messages[message_title] = m + return Reference(**{"$ref": f"#/components/messages/{message_title}"}) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/__init__.py b/faststream/specification/asyncapi/v2_6_0/schema/__init__.py new file mode 100644 index 0000000000..e0cbcbd7b2 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/__init__.py @@ -0,0 +1,31 @@ +from .channels import Channel +from .components import Components +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", + "Contact", + "CorrelationId", + "ExternalDocs", + "License", + "Message", + "Operation", + "Parameter", + "Reference", + "Server", + "ServerVariable", + "Tag", +) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/bindings/__init__.py b/faststream/specification/asyncapi/v2_6_0/schema/bindings/__init__.py new file mode 100644 index 0000000000..84b0fa22e8 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/__init__.py @@ -0,0 +1,6 @@ +from .main import ChannelBinding, OperationBinding + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) 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 new file mode 100644 index 0000000000..8555fd981a --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/__init__.py @@ -0,0 +1,7 @@ +from .channel import ChannelBinding +from .operation import OperationBinding + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) 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 new file mode 100644 index 0000000000..541de4168b --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/channel.py @@ -0,0 +1,144 @@ +"""AsyncAPI AMQP bindings. + +References: https://github.com/asyncapi/bindings/tree/master/amqp +""" + +from typing import Literal, overload + +from pydantic import BaseModel, Field +from typing_extensions import Self + +from faststream.specification.schema.bindings import amqp + + +class Queue(BaseModel): + """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 = "/" + + @overload + @classmethod + 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: amqp.Queue | None, vhost: str) -> Self | None: + if binding is None: + return None + + return cls( + name=binding.name, + durable=binding.durable, + exclusive=binding.exclusive, + autoDelete=binding.auto_delete, + vhost=vhost, + ) + + +class Exchange(BaseModel): + """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 "/" + """ + + name: str | None = None + type: Literal[ + "default", + "direct", + "topic", + "fanout", + "headers", + "x-delayed-message", + "x-consistent-hash", + "x-modulus-hash", + ] + durable: bool | None = None + autoDelete: bool | None = 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: amqp.Exchange | None, vhost: str) -> Self | None: + if binding is None: + return None + + return cls( + name=binding.name, + type=binding.type, + durable=binding.durable, + autoDelete=binding.auto_delete, + vhost=vhost, + ) + + +class ChannelBinding(BaseModel): + """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"] = Field(..., alias="is") + bindingVersion: str = "0.2.0" + queue: Queue | None = None + exchange: Exchange | None = None + + @classmethod + def from_sub(cls, binding: amqp.ChannelBinding | None) -> Self | None: + if binding is None: + return None + + return cls( + **{ + "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: amqp.ChannelBinding | None) -> Self | None: + if binding is None: + return None + + 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 new file mode 100644 index 0000000000..6b8cea1ded --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/amqp/operation.py @@ -0,0 +1,58 @@ +"""AsyncAPI AMQP bindings. + +References: https://github.com/asyncapi/bindings/tree/master/amqp +""" + + +from pydantic import BaseModel, PositiveInt +from typing_extensions import Self + +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 + """ + + cc: str | None = None + ack: bool + replyTo: str | None = None + deliveryMode: int | None = None + mandatory: bool | None = None + priority: PositiveInt | None = None + + bindingVersion: str = "0.2.0" + + @classmethod + def from_sub(cls, binding: amqp.OperationBinding | None) -> Self | None: + if not binding: + return None + + 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, + ) + + @classmethod + def from_pub(cls, binding: amqp.OperationBinding | None) -> Self | None: + if not binding: + return None + + 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 new file mode 100644 index 0000000000..8555fd981a --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/__init__.py @@ -0,0 +1,7 @@ +from .channel import ChannelBinding +from .operation import OperationBinding + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) 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 new file mode 100644 index 0000000000..32ac810c93 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/channel.py @@ -0,0 +1,51 @@ +"""AsyncAPI Kafka bindings. + +References: https://github.com/asyncapi/bindings/tree/master/kafka +""" + + +from pydantic import BaseModel, PositiveInt +from typing_extensions import Self + +from faststream.specification.schema.bindings import kafka + + +class ChannelBinding(BaseModel): + """A class to represent a channel binding. + + Attributes: + 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: str | None = None + partitions: PositiveInt | None = None + replicas: PositiveInt | None = None + bindingVersion: str = "0.4.0" + + # TODO: + # topicConfiguration + + @classmethod + def from_sub(cls, binding: kafka.ChannelBinding | None) -> Self | None: + if binding is None: + return None + + return cls( + topic=binding.topic, + partitions=binding.partitions, + replicas=binding.replicas, + ) + + @classmethod + def from_pub(cls, binding: kafka.ChannelBinding | None) -> Self | None: + if binding is None: + return None + + 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 new file mode 100644 index 0000000000..594cfbac48 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/kafka/operation.py @@ -0,0 +1,49 @@ +"""AsyncAPI Kafka bindings. + +References: https://github.com/asyncapi/bindings/tree/master/kafka +""" + + +from pydantic import BaseModel +from typing_extensions import Self + +from faststream._internal.basic_types import AnyDict +from faststream.specification.schema.bindings import kafka + + +class OperationBinding(BaseModel): + """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") + """ + + groupId: AnyDict | None = None + clientId: AnyDict | None = None + replyTo: AnyDict | None = None + bindingVersion: str = "0.4.0" + + @classmethod + def from_sub(cls, binding: kafka.OperationBinding | None) -> Self | None: + if not binding: + return None + + return cls( + groupId=binding.group_id, + clientId=binding.client_id, + replyTo=binding.reply_to, + ) + + @classmethod + def from_pub(cls, binding: kafka.OperationBinding | None) -> Self | None: + if not binding: + return None + + 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 new file mode 100644 index 0000000000..8555fd981a --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/__init__.py @@ -0,0 +1,7 @@ +from .channel import ChannelBinding +from .operation import OperationBinding + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) 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 new file mode 100644 index 0000000000..f06bde6d30 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/channel.py @@ -0,0 +1,116 @@ +from typing import overload + +from pydantic import BaseModel +from typing_extensions import Self + +from faststream._internal._compat import PYDANTIC_V2 +from faststream.specification.asyncapi.v2_6_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: amqp_bindings.ChannelBinding | None = None + kafka: kafka_bindings.ChannelBinding | None = None + sqs: sqs_bindings.ChannelBinding | None = None + nats: nats_bindings.ChannelBinding | None = None + redis: redis_bindings.ChannelBinding | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @overload + @classmethod + def from_sub(cls, binding: None) -> None: ... + + @overload + @classmethod + def from_sub(cls, binding: SpecBinding) -> Self: ... + + @classmethod + def from_sub(cls, binding: SpecBinding | None) -> Self | None: + 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: SpecBinding | None) -> Self | None: + 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 new file mode 100644 index 0000000000..99361cc450 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/main/operation.py @@ -0,0 +1,116 @@ +from typing import overload + +from pydantic import BaseModel +from typing_extensions import Self + +from faststream._internal._compat import PYDANTIC_V2 +from faststream.specification.asyncapi.v2_6_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 OperationBinding as SpecBinding + + +class OperationBinding(BaseModel): + """A class to represent an operation binding. + + Attributes: + amqp : AMQP operation binding (optional) + kafka : Kafka operation binding (optional) + sqs : SQS operation binding (optional) + nats : NATS operation binding (optional) + redis : Redis operation binding (optional) + """ + + amqp: amqp_bindings.OperationBinding | None = None + kafka: kafka_bindings.OperationBinding | None = None + sqs: sqs_bindings.OperationBinding | None = None + nats: nats_bindings.OperationBinding | None = None + redis: redis_bindings.OperationBinding | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @overload + @classmethod + def from_sub(cls, binding: None) -> None: ... + + @overload + @classmethod + def from_sub(cls, binding: SpecBinding) -> Self: ... + + @classmethod + def from_sub(cls, binding: SpecBinding | None) -> Self | None: + 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: SpecBinding | None) -> Self | None: + 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 new file mode 100644 index 0000000000..8555fd981a --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/__init__.py @@ -0,0 +1,7 @@ +from .channel import ChannelBinding +from .operation import OperationBinding + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) 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 new file mode 100644 index 0000000000..593dd567b6 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/channel.py @@ -0,0 +1,46 @@ +"""AsyncAPI NATS bindings. + +References: https://github.com/asyncapi/bindings/tree/master/nats +""" + + +from pydantic import BaseModel +from typing_extensions import Self + +from faststream.specification.schema.bindings import nats + + +class ChannelBinding(BaseModel): + """A class to represent channel binding. + + 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: str | None = None + bindingVersion: str = "custom" + + @classmethod + def from_sub(cls, binding: nats.ChannelBinding | None) -> Self | None: + if binding is None: + return None + + return cls( + subject=binding.subject, + queue=binding.queue, + bindingVersion="custom", + ) + + @classmethod + def from_pub(cls, binding: nats.ChannelBinding | None) -> Self | None: + if binding is None: + return None + + 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 new file mode 100644 index 0000000000..aacf3e8511 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/nats/operation.py @@ -0,0 +1,41 @@ +"""AsyncAPI NATS bindings. + +References: https://github.com/asyncapi/bindings/tree/master/nats +""" + + +from pydantic import BaseModel +from typing_extensions import Self + +from faststream._internal.basic_types import AnyDict +from faststream.specification.schema.bindings import nats + + +class OperationBinding(BaseModel): + """A class to represent an operation binding. + + Attributes: + replyTo : optional dictionary containing reply information + bindingVersion : version of the binding (default is "custom") + """ + + replyTo: AnyDict | None = None + bindingVersion: str = "custom" + + @classmethod + def from_sub(cls, binding: nats.OperationBinding | None) -> Self | None: + if not binding: + return None + + return cls( + replyTo=binding.reply_to, + ) + + @classmethod + def from_pub(cls, binding: nats.OperationBinding | None) -> Self | None: + if not binding: + return None + + 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 new file mode 100644 index 0000000000..8555fd981a --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/__init__.py @@ -0,0 +1,7 @@ +from .channel import ChannelBinding +from .operation import OperationBinding + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) 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 new file mode 100644 index 0000000000..77fa7cdd21 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/channel.py @@ -0,0 +1,50 @@ +"""AsyncAPI Redis bindings. + +References: https://github.com/asyncapi/bindings/tree/master/redis +""" + + +from pydantic import BaseModel +from typing_extensions import Self + +from faststream.specification.schema.bindings import redis + + +class ChannelBinding(BaseModel): + """A class to represent channel binding. + + Attributes: + channel : the channel name + method : the method used for binding (ssubscribe, psubscribe, subscribe) + bindingVersion : the version of the binding + """ + + channel: str + method: str | None = None + groupName: str | None = None + consumerName: str | None = None + bindingVersion: str = "custom" + + @classmethod + def from_sub(cls, binding: redis.ChannelBinding | None) -> Self | None: + if binding is None: + return None + + return cls( + channel=binding.channel, + method=binding.method, + groupName=binding.group_name, + consumerName=binding.consumer_name, + ) + + @classmethod + def from_pub(cls, binding: redis.ChannelBinding | None) -> Self | None: + if binding is None: + return None + + 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 new file mode 100644 index 0000000000..52a5d2b833 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/redis/operation.py @@ -0,0 +1,41 @@ +"""AsyncAPI Redis bindings. + +References: https://github.com/asyncapi/bindings/tree/master/redis +""" + + +from pydantic import BaseModel +from typing_extensions import Self + +from faststream._internal.basic_types import AnyDict +from faststream.specification.schema.bindings import redis + + +class OperationBinding(BaseModel): + """A class to represent an operation binding. + + Attributes: + replyTo : optional dictionary containing reply information + bindingVersion : version of the binding (default is "custom") + """ + + replyTo: AnyDict | None = None + bindingVersion: str = "custom" + + @classmethod + def from_sub(cls, binding: redis.OperationBinding | None) -> Self | None: + if not binding: + return None + + return cls( + replyTo=binding.reply_to, + ) + + @classmethod + def from_pub(cls, binding: redis.OperationBinding | None) -> Self | None: + if not binding: + return None + + 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 new file mode 100644 index 0000000000..33cdca3a8b --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/__init__.py @@ -0,0 +1,4 @@ +from .channel import ChannelBinding +from .operation import OperationBinding + +__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 new file mode 100644 index 0000000000..3145805c65 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/channel.py @@ -0,0 +1,36 @@ +"""AsyncAPI SQS bindings. + +References: https://github.com/asyncapi/bindings/tree/master/sqs +""" + +from pydantic import BaseModel +from typing_extensions import Self + +from faststream._internal.basic_types import AnyDict +from faststream.specification.schema.bindings import sqs + + +class ChannelBinding(BaseModel): + """A class to represent channel binding. + + Attributes: + queue : a dictionary representing the queue + bindingVersion : a string representing the binding version (default: "custom") + """ + + queue: AnyDict + bindingVersion: str = "custom" + + @classmethod + def from_pub(cls, binding: sqs.ChannelBinding) -> Self: + return cls( + queue=binding.queue, + bindingVersion=binding.bindingVersion, + ) + + @classmethod + def from_sub(cls, binding: sqs.ChannelBinding) -> Self: + return cls( + queue=binding.queue, + bindingVersion=binding.bindingVersion, + ) 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 new file mode 100644 index 0000000000..3c321a1f35 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/bindings/sqs/operation.py @@ -0,0 +1,37 @@ +"""AsyncAPI SQS bindings. + +References: https://github.com/asyncapi/bindings/tree/master/sqs +""" + + +from pydantic import BaseModel +from typing_extensions import Self + +from faststream._internal.basic_types import AnyDict +from faststream.specification.schema.bindings import sqs + + +class OperationBinding(BaseModel): + """A class to represent an operation binding. + + Attributes: + replyTo : optional dictionary containing reply information + bindingVersion : version of the binding, default is "custom" + """ + + replyTo: AnyDict | None = None + bindingVersion: str = "custom" + + @classmethod + def from_pub(cls, binding: sqs.OperationBinding) -> Self: + return cls( + replyTo=binding.replyTo, + bindingVersion=binding.bindingVersion, + ) + + @classmethod + def from_sub(cls, binding: sqs.OperationBinding) -> Self: + return cls( + replyTo=binding.replyTo, + bindingVersion=binding.bindingVersion, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/channels.py b/faststream/specification/asyncapi/v2_6_0/schema/channels.py new file mode 100644 index 0000000000..5ba8bc9ea9 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/channels.py @@ -0,0 +1,62 @@ + +from pydantic import BaseModel +from typing_extensions import Self + +from faststream._internal._compat import PYDANTIC_V2 +from faststream.specification.schema import PublisherSpec, SubscriberSpec + +from .bindings import ChannelBinding +from .operations import Operation + + +class Channel(BaseModel): + """A class to represent a channel. + + 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 + + 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: str | None = None + servers: list[str] | None = None + bindings: ChannelBinding | None = None + subscribe: Operation | None = None + publish: Operation | None = None + + # TODO: + # parameters: Optional[Parameter] = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @classmethod + def from_sub(cls, subscriber: SubscriberSpec) -> Self: + return cls( + description=subscriber.description, + servers=None, + bindings=ChannelBinding.from_sub(subscriber.bindings), + subscribe=None, + publish=Operation.from_sub(subscriber.operation), + ) + + @classmethod + def from_pub(cls, publisher: PublisherSpec) -> Self: + return cls( + description=publisher.description, + servers=None, + bindings=ChannelBinding.from_pub(publisher.bindings), + subscribe=Operation.from_pub(publisher.operation), + publish=None, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/components.py b/faststream/specification/asyncapi/v2_6_0/schema/components.py new file mode 100644 index 0000000000..7816b32481 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/components.py @@ -0,0 +1,48 @@ + +from pydantic import BaseModel + +from faststream._internal._compat import ( + PYDANTIC_V2, +) +from faststream._internal.basic_types import AnyDict +from faststream.specification.asyncapi.v2_6_0.schema.message import Message + + +class Components(BaseModel): + # TODO + # servers + # serverVariables + # channels + """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 + - parameters + - correlationIds + - operationTraits + - messageTraits + - serverBindings + - channelBindings + - operationBindings + - messageBindings + """ + + messages: dict[str, Message] | None = None + schemas: dict[str, AnyDict] | None = None + securitySchemes: dict[str, AnyDict] | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" diff --git a/faststream/specification/asyncapi/v2_6_0/schema/contact.py b/faststream/specification/asyncapi/v2_6_0/schema/contact.py new file mode 100644 index 0000000000..f735f1dedd --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/contact.py @@ -0,0 +1,73 @@ +from typing import cast, 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._internal.utils.data import filter_by_dict +from faststream.specification.schema.extra import ( + Contact as SpecContact, + ContactDict, +) + + +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 + # Use default values to be able build from dict + url: AnyHttpUrl | None = None + email: EmailStr | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @overload + @classmethod + def from_spec(cls, contact: None) -> None: ... + + @overload + @classmethod + def from_spec(cls, contact: SpecContact) -> Self: ... + + @overload + @classmethod + def from_spec(cls, contact: ContactDict) -> Self: ... + + @overload + @classmethod + def from_spec(cls, contact: AnyDict) -> AnyDict: ... + + @classmethod + def from_spec( + cls, contact: SpecContact | ContactDict | AnyDict | None + ) -> Self | AnyDict | None: + if contact is None: + return None + + if isinstance(contact, SpecContact): + return cls( + name=contact.name, + url=contact.url, + email=contact.email, + ) + + contact = cast("AnyDict", contact) + contact_data, custom_data = filter_by_dict(ContactDict, contact) + + if custom_data: + return 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 new file mode 100644 index 0000000000..5af52f62f2 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/docs.py @@ -0,0 +1,67 @@ +from typing import cast, overload + +from pydantic import AnyHttpUrl, BaseModel +from typing_extensions import Self + +from faststream._internal._compat import PYDANTIC_V2 +from faststream._internal.basic_types import AnyDict +from faststream._internal.utils.data import filter_by_dict +from faststream.specification.schema.extra import ( + ExternalDocs as SpecDocs, + ExternalDocsDict, +) + + +class ExternalDocs(BaseModel): + """A class to represent external documentation. + + 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: str | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @overload + @classmethod + def from_spec(cls, docs: None) -> None: ... + + @overload + @classmethod + def from_spec(cls, docs: SpecDocs) -> Self: ... + + @overload + @classmethod + def from_spec(cls, docs: ExternalDocsDict) -> Self: ... + + @overload + @classmethod + def from_spec(cls, docs: AnyDict) -> AnyDict: ... + + @classmethod + def from_spec( + cls, docs: SpecDocs | ExternalDocsDict | AnyDict | None + ) -> Self | AnyDict | None: + if docs is None: + return None + + if isinstance(docs, SpecDocs): + return cls(url=docs.url, description=docs.description) + + docs = cast("AnyDict", docs) + docs_data, custom_data = filter_by_dict(ExternalDocsDict, docs) + + if custom_data: + return 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 new file mode 100644 index 0000000000..69b0a71331 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/info.py @@ -0,0 +1,24 @@ + +from pydantic import AnyHttpUrl + +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 BaseApplicationInfo + + +class ApplicationInfo(BaseApplicationInfo): + """A class to represent application information. + + Attributes: + title : title of the information + 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: AnyHttpUrl | None = None + contact: Contact | AnyDict | None = None + license: License | AnyDict | None = None diff --git a/faststream/specification/asyncapi/v2_6_0/schema/license.py b/faststream/specification/asyncapi/v2_6_0/schema/license.py new file mode 100644 index 0000000000..98e83e6f9d --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/license.py @@ -0,0 +1,73 @@ +from typing import cast, overload + +from pydantic import AnyHttpUrl, BaseModel +from typing_extensions import Self + +from faststream._internal._compat import PYDANTIC_V2 +from faststream._internal.basic_types import AnyDict +from faststream._internal.utils.data import filter_by_dict +from faststream.specification.schema.extra import ( + License as SpecLicense, + LicenseDict, +) + + +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 + # Use default values to be able build from dict + url: AnyHttpUrl | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @overload + @classmethod + def from_spec(cls, license: None) -> None: ... + + @overload + @classmethod + def from_spec(cls, license: SpecLicense) -> Self: ... + + @overload + @classmethod + def from_spec(cls, license: LicenseDict) -> Self: ... + + @overload + @classmethod + def from_spec(cls, license: AnyDict) -> AnyDict: ... + + @classmethod + def from_spec( + cls, license: SpecLicense | LicenseDict | AnyDict | None + ) -> Self | AnyDict | None: + if license is None: + return None + + if isinstance(license, SpecLicense): + return cls( + name=license.name, + url=license.url, + ) + + license = cast("AnyDict", license) + license_data, custom_data = filter_by_dict(LicenseDict, license) + + if custom_data: + return 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 new file mode 100644 index 0000000000..0e5da7c93e --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/message.py @@ -0,0 +1,90 @@ + +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.asyncapi.v2_6_0.schema.tag import Tag +from faststream.specification.schema.message import Message as SpecMessage + + +class CorrelationId(BaseModel): + """A class to represent a correlation ID. + + 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: str | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + +class Message(BaseModel): + """A class to represent a message. + + 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 + """ + + title: str | None = None + name: str | None = None + summary: str | None = None + description: str | None = None + messageId: str | None = None + correlationId: CorrelationId | None = None + contentType: str | None = None + + payload: AnyDict + # TODO: + # headers + # schemaFormat + # bindings + # examples + # traits + + tags: list[Tag | AnyDict] | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @classmethod + def from_spec(cls, message: SpecMessage) -> Self: + return cls( + title=message.title, + payload=message.payload, + correlationId=CorrelationId( + description=None, + location="$message.header#/correlation_id", + ), + name=None, + summary=None, + description=None, + messageId=None, + contentType=None, + tags=None, + ) diff --git a/faststream/specification/asyncapi/v2_6_0/schema/operations.py b/faststream/specification/asyncapi/v2_6_0/schema/operations.py new file mode 100644 index 0000000000..84ba5be788 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/operations.py @@ -0,0 +1,73 @@ + +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.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): + """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 + """ + + operationId: str | None = None + summary: str | None = None + description: str | None = None + + bindings: OperationBinding | None = None + + message: Message | Reference + + security: dict[str, list[str]] | None = None + + # TODO + # traits + + tags: list[Tag | AnyDict] | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @classmethod + def from_sub(cls, operation: OperationSpec) -> Self: + return cls( + message=Message.from_spec(operation.message), + bindings=OperationBinding.from_sub(operation.bindings), + operationId=None, + summary=None, + description=None, + tags=None, + security=None, + ) + + @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 new file mode 100644 index 0000000000..af2a527d6b --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/schema.py @@ -0,0 +1,37 @@ +from typing import Literal + +from faststream._internal.basic_types import AnyDict +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 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 BaseApplicationSchema + + +class ApplicationSchema(BaseApplicationSchema): + """A class to represent an application schema. + + Attributes: + asyncapi : version of the async API + id : optional ID + defaultContentType : optional default content type + info : information about the schema + servers : optional dictionary of servers + channels : dictionary of channels + components : optional components of the schema + tags : optional list of tags + externalDocs : optional external documentation + """ + + info: ApplicationInfo + + asyncapi: Literal["2.6.0"] | str + id: str | None = None + defaultContentType: str | None = None + servers: dict[str, Server] | None = None + channels: dict[str, Channel] + components: Components | None = None + tags: list[Tag | AnyDict] | None = None + externalDocs: ExternalDocs | AnyDict | None = None diff --git a/faststream/specification/asyncapi/v2_6_0/schema/servers.py b/faststream/specification/asyncapi/v2_6_0/schema/servers.py new file mode 100644 index 0000000000..1d26cb5dd8 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/servers.py @@ -0,0 +1,68 @@ + +from pydantic import BaseModel + +from faststream._internal._compat import PYDANTIC_V2 +from faststream._internal.basic_types import AnyDict +from faststream.specification.asyncapi.v2_6_0.schema.tag import Tag +from faststream.specification.asyncapi.v2_6_0.schema.utils import Reference + +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: list[str] | None = None + default: str | None = None + description: str | None = None + examples: list[str] | None = 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 + bindings : optional server binding + + Note: + The attributes `description`, `protocolVersion`, `tags`, `security`, `variables`, and `bindings` are all optional. + """ + + url: str + protocol: str + protocolVersion: str | None + description: str | None = None + tags: list[Tag | AnyDict] | None = None + security: SecurityRequirement | None = None + + variables: dict[str, ServerVariable | Reference] | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" diff --git a/faststream/specification/asyncapi/v2_6_0/schema/tag.py b/faststream/specification/asyncapi/v2_6_0/schema/tag.py new file mode 100644 index 0000000000..5f2da8f4dc --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/tag.py @@ -0,0 +1,69 @@ +from typing import cast, overload + +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._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, +) + + +class Tag(BaseModel): + """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 + # Use default values to be able build from dict + description: str | None = None + externalDocs: ExternalDocs | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @overload + @classmethod + def from_spec(cls, tag: SpecTag) -> Self: ... + + @overload + @classmethod + def from_spec(cls, tag: TagDict) -> Self: ... + + @overload + @classmethod + def from_spec(cls, tag: AnyDict) -> AnyDict: ... + + @classmethod + def from_spec(cls, tag: SpecTag | TagDict | AnyDict) -> Self | AnyDict: + if isinstance(tag, SpecTag): + return cls( + name=tag.name, + description=tag.description, + externalDocs=ExternalDocs.from_spec(tag.external_docs), + ) + + tag = cast("AnyDict", tag) + tag_data, custom_data = filter_by_dict(TagDict, tag) + + if custom_data: + return 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 new file mode 100644 index 0000000000..6d492ffeb5 --- /dev/null +++ b/faststream/specification/asyncapi/v2_6_0/schema/utils.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel, Field + + +class Reference(BaseModel): + """A class to represent a reference. + + Attributes: + ref : the reference string + """ + + ref: str = Field(..., alias="$ref") + + +class Parameter(BaseModel): + """A class to represent a parameter.""" + + # TODO diff --git a/faststream/specification/asyncapi/v3_0_0/__init__.py b/faststream/specification/asyncapi/v3_0_0/__init__.py new file mode 100644 index 0000000000..490431d760 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/__init__.py @@ -0,0 +1,7 @@ +from .facade import AsyncAPI3 +from .generate import get_app_schema + +__all__ = ( + "AsyncAPI3", + "get_app_schema", +) diff --git a/faststream/specification/asyncapi/v3_0_0/facade.py b/faststream/specification/asyncapi/v3_0_0/facade.py new file mode 100644 index 0000000000..c719760804 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/facade.py @@ -0,0 +1,75 @@ +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Optional, Union + +from faststream.specification.base.specification import Specification + +from .generate import get_app_schema +from .schema import ApplicationSchema + +if TYPE_CHECKING: + from faststream._internal.basic_types import AnyDict, AnyHttpUrl + from faststream._internal.broker import BrokerUsecase + from faststream.specification.schema.extra import ( + Contact, + ContactDict, + ExternalDocs, + ExternalDocsDict, + License, + LicenseDict, + Tag, + TagDict, + ) + + +class AsyncAPI3(Specification): + def __init__( + self, + broker: "BrokerUsecase[Any, Any]", + /, + title: str = "FastStream", + app_version: str = "0.1.0", + schema_version: str = "3.0.0", + description: str = "", + terms_of_service: Optional["AnyHttpUrl"] = None, + contact: Union["Contact", "ContactDict", "AnyDict"] | None = None, + license: Union["License", "LicenseDict", "AnyDict"] | None = None, + identifier: str | None = None, + tags: Sequence[Union["Tag", "TagDict", "AnyDict"]] = (), + external_docs: Union["ExternalDocs", "ExternalDocsDict", "AnyDict"] | None = None, + ) -> None: + self.broker = broker + self.title = title + self.app_version = app_version + self.schema_version = schema_version + self.description = description + self.terms_of_service = terms_of_service + self.contact = contact + self.license = license + self.identifier = identifier + self.tags = tags + self.external_docs = external_docs + + def to_json(self) -> str: + return self.schema.to_json() + + def to_jsonable(self) -> Any: + return self.schema.to_jsonable() + + def to_yaml(self) -> str: + return self.schema.to_yaml() + + @property + def schema(self) -> ApplicationSchema: + return get_app_schema( + self.broker, + title=self.title, + app_version=self.app_version, + schema_version=self.schema_version, + description=self.description, + terms_of_service=self.terms_of_service, + contact=self.contact, + license=self.license, + identifier=self.identifier, + tags=self.tags, + external_docs=self.external_docs, + ) diff --git a/faststream/specification/asyncapi/v3_0_0/generate.py b/faststream/specification/asyncapi/v3_0_0/generate.py new file mode 100644 index 0000000000..939d43c547 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/generate.py @@ -0,0 +1,257 @@ +import warnings +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Optional, Union +from urllib.parse import urlparse + +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, move_pydantic_refs +from faststream.specification.asyncapi.v3_0_0.schema import ( + ApplicationInfo, + ApplicationSchema, + Channel, + Components, + Contact, + ExternalDocs, + License, + Message, + Operation, + Reference, + Server, + Tag, +) + +if TYPE_CHECKING: + from faststream._internal.broker import BrokerUsecase + from faststream._internal.types import ConnectionType, MsgType + from faststream.specification.schema.extra import ( + Contact as SpecContact, + ContactDict, + ExternalDocs as SpecDocs, + ExternalDocsDict, + License as SpecLicense, + LicenseDict, + Tag as SpecTag, + TagDict, + ) + + +def get_app_schema( + broker: "BrokerUsecase[Any, Any]", + /, + title: str, + app_version: str, + schema_version: str, + description: str, + terms_of_service: Optional["AnyHttpUrl"], + contact: Union["SpecContact", "ContactDict", "AnyDict"] | None, + license: Union["SpecLicense", "LicenseDict", "AnyDict"] | None, + identifier: str | None, + tags: Sequence[Union["SpecTag", "TagDict", "AnyDict"]] | None, + external_docs: Union["SpecDocs", "ExternalDocsDict", "AnyDict"] | None, +) -> ApplicationSchema: + """Get the application schema.""" + servers = get_broker_server(broker) + channels, operations = get_broker_channels(broker) + + messages: dict[str, Message] = {} + payloads: dict[str, AnyDict] = {} + + for channel in channels.values(): + channel.servers = [ + {"$ref": f"#/servers/{server_name}"} for server_name in list(servers.keys()) + ] + + for channel_name, channel in channels.items(): + msgs: dict[str, Message | Reference] = {} + for message_name, message in channel.messages.items(): + assert isinstance(message, Message) + + msgs[message_name] = _resolve_msg_payloads( + message_name, + message, + channel_name, + payloads, + messages, + ) + + channel.messages = msgs + + return ApplicationSchema( + info=ApplicationInfo( + title=title, + version=app_version, + description=description, + termsOfService=terms_of_service, + contact=Contact.from_spec(contact), + license=License.from_spec(license), + tags=[Tag.from_spec(tag) for tag in tags] or None if tags else None, + externalDocs=ExternalDocs.from_spec(external_docs), + ), + asyncapi=schema_version, + defaultContentType=ContentTypes.JSON.value, + id=identifier, + servers=servers, + channels=channels, + operations=operations, + components=Components( + messages=messages, + schemas=payloads, + securitySchemes=None + if broker.specification.security is None + else broker.specification.security.get_schema(), + ), + ) + + +def get_broker_server( + broker: "BrokerUsecase[MsgType, ConnectionType]", +) -> dict[str, Server]: + """Get the broker server for an application.""" + specification = broker.specification + + servers = {} + + tags: list[Tag | AnyDict] | None = None + if specification.tags: + tags = [Tag.from_spec(tag) for tag in specification.tags] + + broker_meta: AnyDict = { + "protocol": specification.protocol, + "protocolVersion": specification.protocol_version, + "description": specification.description, + "tags": tags, + # TODO + # "variables": "", + # "bindings": "", + } + + if specification.security is not None: + broker_meta["security"] = specification.security.get_requirement() + + single_server = len(specification.url) == 1 + for i, broker_url in enumerate(specification.url, 1): + server_url = broker_url if "://" in broker_url else f"//{broker_url}" + + parsed_url = urlparse(server_url) + server_name = "development" if single_server else f"Server{i}" + servers[server_name] = Server( + host=parsed_url.netloc, + pathname=parsed_url.path, + **broker_meta, + ) + + return servers + + +def get_broker_channels( + broker: "BrokerUsecase[MsgType, ConnectionType]", +) -> tuple[dict[str, Channel], dict[str, Operation]]: + """Get the broker channels for an application.""" + channels = {} + operations = {} + + for sub in filter(lambda s: s.specification.include_in_schema, broker.subscribers): + for sub_key, sub_channel in sub.schema().items(): + channel_obj = Channel.from_sub(sub_key, sub_channel) + + channel_key = clear_key(sub_key) + if channel_key in channels: + warnings.warn( + f"Overwrite channel handler, channels have the same names: `{channel_key}`", + RuntimeWarning, + stacklevel=1, + ) + + 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=sub_channel.operation, + ) + + for pub in filter(lambda p: p.specification.include_in_schema, broker.publishers): + for pub_key, pub_channel in pub.schema().items(): + channel_obj = Channel.from_pub(pub_key, pub_channel) + + channel_key = clear_key(pub_key) + if channel_key in channels: + warnings.warn( + f"Overwrite channel handler, channels have the same names: `{channel_key}`", + RuntimeWarning, + stacklevel=1, + ) + 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=pub_channel.operation, + ) + + return channels, operations + + +def _resolve_msg_payloads( + message_name: str, + m: Message, + channel_name: str, + payloads: AnyDict, + messages: AnyDict, +) -> Reference: + assert isinstance(m.payload, dict) + + m.payload = move_pydantic_refs(m.payload, DEF_KEY) + + message_name = clear_key(message_name) + channel_name = clear_key(channel_name) + + if DEF_KEY in m.payload: + payloads.update(m.payload.pop(DEF_KEY)) + + one_of = m.payload.get("oneOf", None) + if isinstance(one_of, dict): + one_of_list = [] + processed_payloads: dict[str, AnyDict] = {} + for name, payload in one_of.items(): + processed_payloads[clear_key(name)] = payload + one_of_list.append(Reference(**{"$ref": f"#/components/schemas/{name}"})) + + payloads.update(processed_payloads) + m.payload["oneOf"] = one_of_list + assert m.title + messages[clear_key(m.title)] = m + return Reference( + **{"$ref": f"#/components/messages/{channel_name}:{message_name}"}, + ) + + payloads.update(m.payload.pop(DEF_KEY, {})) + payload_name = m.payload.get("title", f"{channel_name}:{message_name}:Payload") + payload_name = clear_key(payload_name) + + if payload_name in payloads and payloads[payload_name] != m.payload: + warnings.warn( + f"Overwriting the message schema, data types have the same name: `{payload_name}`", + RuntimeWarning, + stacklevel=1, + ) + + payloads[payload_name] = m.payload + m.payload = {"$ref": f"#/components/schemas/{payload_name}"} + assert m.title + messages[clear_key(m.title)] = m + return Reference( + **{"$ref": f"#/components/messages/{channel_name}:{message_name}"}, + ) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/__init__.py b/faststream/specification/asyncapi/v3_0_0/schema/__init__.py new file mode 100644 index 0000000000..e0cbcbd7b2 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/__init__.py @@ -0,0 +1,31 @@ +from .channels import Channel +from .components import Components +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", + "Contact", + "CorrelationId", + "ExternalDocs", + "License", + "Message", + "Operation", + "Parameter", + "Reference", + "Server", + "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 new file mode 100644 index 0000000000..c304608c5b --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/__init__.py @@ -0,0 +1,9 @@ +from .main import ( + ChannelBinding, + OperationBinding, +) + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) 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 new file mode 100644 index 0000000000..8555fd981a --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/__init__.py @@ -0,0 +1,7 @@ +from .channel import ChannelBinding +from .operation import OperationBinding + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) 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 new file mode 100644 index 0000000000..11fb35cd01 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/channel.py @@ -0,0 +1,39 @@ + +from typing_extensions import Self + +from faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp import ( + ChannelBinding as V2Binding, +) +from faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp.channel import ( + Exchange, + Queue, +) +from faststream.specification.schema.bindings import amqp + + +class ChannelBinding(V2Binding): + bindingVersion: str = "0.3.0" + + @classmethod + def from_sub(cls, binding: amqp.ChannelBinding | None) -> Self | None: + if binding is None: + return None + + return cls( + **{ + "is": "queue", + "queue": Queue.from_spec(binding.queue, binding.virtual_host), + }, + ) + + @classmethod + def from_pub(cls, binding: amqp.ChannelBinding | None) -> Self | None: + if binding is None: + return None + + return cls( + **{ + "is": "routingKey", + "exchange": Exchange.from_spec(binding.exchange, binding.virtual_host), + }, + ) 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 new file mode 100644 index 0000000000..57063837c3 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/amqp/operation.py @@ -0,0 +1,53 @@ +"""AsyncAPI AMQP bindings. + +References: https://github.com/asyncapi/bindings/tree/master/amqp +""" + + +from pydantic import BaseModel, PositiveInt +from typing_extensions import Self + +from faststream.specification.schema.bindings import amqp + + +class OperationBinding(BaseModel): + cc: list[str] | None = None + ack: bool + replyTo: str | None = None + deliveryMode: int | None = None + mandatory: bool | None = None + priority: PositiveInt | None = None + + bindingVersion: str = "0.3.0" + + @classmethod + def from_sub(cls, binding: amqp.OperationBinding | None) -> Self | None: + if not binding: + return None + + return cls( + cc=[binding.routing_key] + if (binding.routing_key and 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, + ) + + @classmethod + def from_pub(cls, binding: amqp.OperationBinding | None) -> Self | None: + if not binding: + return None + + 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 new file mode 100644 index 0000000000..8555fd981a --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/__init__.py @@ -0,0 +1,7 @@ +from .channel import ChannelBinding +from .operation import OperationBinding + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) 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 new file mode 100644 index 0000000000..9a92f2dd27 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/channel.py @@ -0,0 +1,99 @@ + +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: amqp_bindings.ChannelBinding | None = None + kafka: kafka_bindings.ChannelBinding | None = None + sqs: sqs_bindings.ChannelBinding | None = None + nats: nats_bindings.ChannelBinding | None = None + redis: redis_bindings.ChannelBinding | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @classmethod + def from_sub(cls, binding: SpecBinding | None) -> Self | None: + 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: SpecBinding | None) -> Self | None: + 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/v3_0_0/schema/bindings/main/operation.py b/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/operation.py new file mode 100644 index 0000000000..22ed2407b5 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/bindings/main/operation.py @@ -0,0 +1,99 @@ + +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 OperationBinding as SpecBinding + + +class OperationBinding(BaseModel): + """A class to represent an operation binding. + + Attributes: + amqp : AMQP operation binding (optional) + kafka : Kafka operation binding (optional) + sqs : SQS operation binding (optional) + nats : NATS operation binding (optional) + redis : Redis operation binding (optional) + """ + + amqp: amqp_bindings.OperationBinding | None = None + kafka: kafka_bindings.OperationBinding | None = None + sqs: sqs_bindings.OperationBinding | None = None + nats: nats_bindings.OperationBinding | None = None + redis: redis_bindings.OperationBinding | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @classmethod + def from_sub(cls, binding: SpecBinding | None) -> Self | None: + 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: SpecBinding | None) -> Self | None: + 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 new file mode 100644 index 0000000000..3f72c5ae69 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/channels.py @@ -0,0 +1,73 @@ + +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 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): + """A class to represent a channel. + + Attributes: + address: A string representation of this channel's address. + description : optional description of the channel + servers : optional list of servers associated with the channel + bindings : optional channel binding + parameters : optional parameters associated with the channel + + 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 + description: str | None = None + servers: list[dict[str, str]] | None = None + messages: dict[str, Message | Reference] + bindings: ChannelBinding | None = None + + # TODO: + # parameters: Optional[Parameter] = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @classmethod + 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( + description=subscriber.description, + address=address, + messages={ + "SubscribeMessage": Message.from_spec(message), + }, + bindings=ChannelBinding.from_sub(subscriber.bindings), + servers=None, + ) + + @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/components.py b/faststream/specification/asyncapi/v3_0_0/schema/components.py new file mode 100644 index 0000000000..31ef6d311e --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/components.py @@ -0,0 +1,55 @@ + +from pydantic import BaseModel + +from faststream._internal._compat import PYDANTIC_V2 +from faststream._internal.basic_types import AnyDict +from faststream.specification.asyncapi.v2_6_0.schema.message import Message + + +class Components(BaseModel): + # TODO + # servers + # serverVariables + # channels + """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 + - parameters + - correlationIds + - operationTraits + - messageTraits + - serverBindings + - channelBindings + - operationBindings + - messageBindings + + """ + + messages: dict[str, Message] | None = None + schemas: dict[str, AnyDict] | None = None + securitySchemes: dict[str, AnyDict] | None = None + # parameters + # correlationIds + # operationTraits + # messageTraits + # serverBindings + # channelBindings + # operationBindings + # messageBindings + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" 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 new file mode 100644 index 0000000000..a0c5b78069 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/info.py @@ -0,0 +1,34 @@ +from typing import ( + Union, +) + +from pydantic import AnyHttpUrl + +from faststream._internal.basic_types import ( + AnyDict, +) +from faststream.specification.asyncapi.v2_6_0.schema import ( + Contact, + ExternalDocs, + License, + Tag, +) +from faststream.specification.base.info import BaseApplicationInfo + + +class ApplicationInfo(BaseApplicationInfo): + """A class to represent application information. + + Attributes: + 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 + """ + + termsOfService: AnyHttpUrl | None = None + contact: Contact | AnyDict | None = None + license: License | AnyDict | None = None + tags: list[Union["Tag", "AnyDict"]] | None = None + externalDocs: Union["ExternalDocs", "AnyDict"] | None = None 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 new file mode 100644 index 0000000000..0c272fbbe6 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/operations.py @@ -0,0 +1,93 @@ +from enum import Enum + +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.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): + SEND = "send" + RECEIVE = "receive" + + +class Operation(BaseModel): + """A class to represent an operation. + + Attributes: + 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: Channel | Reference + + summary: str | None = None + description: str | None = None + + bindings: OperationBinding | None = None + + messages: list[Reference] = Field(default_factory=list) + + security: dict[str, list[str]] | None = None + + # TODO + # traits + + tags: list[Tag | AnyDict] | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" + + @classmethod + def from_sub( + cls, + messages: list[Reference], + channel: Reference, + operation: OperationSpec, + ) -> Self: + return cls( + action=Action.RECEIVE, + messages=messages, + channel=channel, + bindings=OperationBinding.from_sub(operation.bindings), + summary=None, + description=None, + security=None, + tags=None, + ) + + @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 new file mode 100644 index 0000000000..643cc5d07e --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/schema.py @@ -0,0 +1,34 @@ +from typing import Literal + +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 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 BaseApplicationSchema + + +class ApplicationSchema(BaseApplicationSchema): + """A class to represent an application schema. + + Attributes: + asyncapi : version of the async API + id : optional ID + defaultContentType : optional default content type + info : information about the schema + servers : optional dictionary of servers + channels : dictionary of channels + components : optional components of the schema + """ + + info: ApplicationInfo + + asyncapi: Literal["3.0.0"] | str = "3.0.0" + id: str | None = None + defaultContentType: str | None = None + servers: dict[str, Server] | None = None + channels: dict[str, Channel] = Field(default_factory=dict) + operations: dict[str, Operation] = Field(default_factory=dict) + components: Components | None = None diff --git a/faststream/specification/asyncapi/v3_0_0/schema/servers.py b/faststream/specification/asyncapi/v3_0_0/schema/servers.py new file mode 100644 index 0000000000..200be95664 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/servers.py @@ -0,0 +1,55 @@ + +from pydantic import BaseModel + +from faststream._internal._compat import PYDANTIC_V2 +from faststream._internal.basic_types import AnyDict +from faststream.specification.asyncapi.v2_6_0.schema import ServerVariable, Tag +from faststream.specification.asyncapi.v2_6_0.schema.utils import Reference + +SecurityRequirement = list[dict[str, list[str]]] + + +__all__ = ( + "Server", + "ServerVariable", +) + + +class Server(BaseModel): + """A class to represent a server. + + Attributes: + host : host of the server + pathname : pathname 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. + + 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". + + """ + + host: str + pathname: str + protocol: str + description: str | None = None + protocolVersion: str | None = None + tags: list[Tag | AnyDict] | None = None + security: SecurityRequirement | None = None + variables: dict[str, ServerVariable | Reference] | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" 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/cli/docs/__init__.py b/faststream/specification/base/__init__.py similarity index 100% rename from faststream/cli/docs/__init__.py rename to faststream/specification/base/__init__.py diff --git a/faststream/specification/base/info.py b/faststream/specification/base/info.py new file mode 100644 index 0000000000..a178044e6a --- /dev/null +++ b/faststream/specification/base/info.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel + +from faststream._internal._compat import PYDANTIC_V2 + + +class BaseApplicationInfo(BaseModel): + """A class to represent basic application information. + + Attributes: + title : application title + version : application version + description : application description + """ + + title: str + version: str + description: str | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" diff --git a/faststream/specification/base/schema.py b/faststream/specification/base/schema.py new file mode 100644 index 0000000000..828e1699b7 --- /dev/null +++ b/faststream/specification/base/schema.py @@ -0,0 +1,48 @@ +from typing import Any + +from pydantic import BaseModel + +from faststream._internal._compat import model_to_json, model_to_jsonable + +from .info import BaseApplicationInfo + + +class BaseApplicationSchema(BaseModel): + """A class to represent a Pydantic-serializable application schema. + + Attributes: + info : information about the schema + + Methods: + to_jsonable() -> Any: Convert the schema to a JSON-serializable object. + to_json() -> str: Convert the schema to a JSON string. + to_yaml() -> str: Convert the schema to a YAML string. + """ + + info: BaseApplicationInfo + + def to_jsonable(self) -> Any: + """Convert the schema to a JSON-serializable object.""" + return model_to_jsonable( + self, + by_alias=True, + exclude_none=True, + ) + + def to_json(self) -> str: + """Convert the schema to a JSON string.""" + return model_to_json( + self, + by_alias=True, + exclude_none=True, + ) + + def to_yaml(self) -> str: + """Convert the schema to a YAML string.""" + from io import StringIO + + import yaml + + io = StringIO(initial_value="", newline="\n") + yaml.dump(self.to_jsonable(), io, sort_keys=False) + return io.getvalue() diff --git a/faststream/specification/base/specification.py b/faststream/specification/base/specification.py new file mode 100644 index 0000000000..e8e674b25e --- /dev/null +++ b/faststream/specification/base/specification.py @@ -0,0 +1,20 @@ +from abc import abstractmethod +from typing import Any, Protocol, runtime_checkable + +from .schema import BaseApplicationSchema + + +@runtime_checkable +class Specification(Protocol): + @property + @abstractmethod + def schema(self) -> BaseApplicationSchema: ... + + def to_json(self) -> str: + return self.schema.to_json() + + def to_jsonable(self) -> Any: + return self.schema.to_jsonable() + + def to_yaml(self) -> str: + return self.schema.to_yaml() diff --git a/faststream/specification/schema/__init__.py b/faststream/specification/schema/__init__.py new file mode 100644 index 0000000000..340cac8ff5 --- /dev/null +++ b/faststream/specification/schema/__init__.py @@ -0,0 +1,31 @@ +from .broker import BrokerSpec +from .extra import ( + Contact, + ContactDict, + ExternalDocs, + ExternalDocsDict, + License, + LicenseDict, + Tag, + TagDict, +) +from .message import Message +from .operation import Operation +from .publisher import PublisherSpec +from .subscriber import SubscriberSpec + +__all__ = ( + "BrokerSpec", + "Contact", + "ContactDict", + "ExternalDocs", + "ExternalDocsDict", + "License", + "LicenseDict", + "Message", + "Operation", + "PublisherSpec", + "SubscriberSpec", + "Tag", + "TagDict", +) diff --git a/faststream/specification/schema/bindings/__init__.py b/faststream/specification/schema/bindings/__init__.py new file mode 100644 index 0000000000..c304608c5b --- /dev/null +++ b/faststream/specification/schema/bindings/__init__.py @@ -0,0 +1,9 @@ +from .main import ( + ChannelBinding, + OperationBinding, +) + +__all__ = ( + "ChannelBinding", + "OperationBinding", +) diff --git a/faststream/specification/schema/bindings/amqp.py b/faststream/specification/schema/bindings/amqp.py new file mode 100644 index 0000000000..0fd9ad30f2 --- /dev/null +++ b/faststream/specification/schema/bindings/amqp.py @@ -0,0 +1,79 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal + +if TYPE_CHECKING: + from faststream.rabbit.schemas import RabbitExchange, RabbitQueue + + +@dataclass +class Queue: + name: str + durable: bool + exclusive: bool + 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: + type: Literal[ + "default", + "direct", + "topic", + "fanout", + "headers", + "x-delayed-message", + "x-consistent-hash", + "x-modulus-hash", + ] + + name: str | None = None + durable: bool | None = None + auto_delete: bool | None = 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: + queue: Queue + exchange: Exchange + virtual_host: str + + +@dataclass +class OperationBinding: + routing_key: str | None + queue: Queue + exchange: Exchange + ack: bool + reply_to: str | None + persist: bool | None + mandatory: bool | None + priority: int | None diff --git a/faststream/asyncapi/schema/bindings/http.py b/faststream/specification/schema/bindings/http.py similarity index 100% rename from faststream/asyncapi/schema/bindings/http.py rename to faststream/specification/schema/bindings/http.py diff --git a/faststream/specification/schema/bindings/kafka.py b/faststream/specification/schema/bindings/kafka.py new file mode 100644 index 0000000000..b96e0b0a83 --- /dev/null +++ b/faststream/specification/schema/bindings/kafka.py @@ -0,0 +1,40 @@ +"""AsyncAPI Kafka bindings. + +References: https://github.com/asyncapi/bindings/tree/master/kafka +""" + +from dataclasses import dataclass +from typing import Any + + +@dataclass +class ChannelBinding: + """A class to represent a channel binding. + + Attributes: + topic : optional string representing the topic + partitions : optional positive integer representing the number of partitions + replicas : optional positive integer representing the number of replicas + """ + + topic: str | None + partitions: int | None + replicas: int | None + + # TODO: + # topicConfiguration + + +@dataclass +class OperationBinding: + """A class to represent an operation binding. + + Attributes: + group_id : optional dictionary representing the group ID + client_id : optional dictionary representing the client ID + reply_to : optional dictionary representing the reply-to + """ + + group_id: dict[str, Any] | None + client_id: dict[str, Any] | None + reply_to: dict[str, Any] | None diff --git a/faststream/specification/schema/bindings/main.py b/faststream/specification/schema/bindings/main.py new file mode 100644 index 0000000000..26efa1a1b0 --- /dev/null +++ b/faststream/specification/schema/bindings/main.py @@ -0,0 +1,50 @@ +from dataclasses import dataclass + +from faststream.specification.schema.bindings import ( + amqp as amqp_bindings, + http as http_bindings, + kafka as kafka_bindings, + nats as nats_bindings, + redis as redis_bindings, + sqs as sqs_bindings, +) + + +@dataclass +class ChannelBinding: + """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)d + redis : Redis channel binding (optional) + """ + + amqp: amqp_bindings.ChannelBinding | None = None + kafka: kafka_bindings.ChannelBinding | None = None + sqs: sqs_bindings.ChannelBinding | None = None + nats: nats_bindings.ChannelBinding | None = None + redis: redis_bindings.ChannelBinding | None = None + + +@dataclass +class OperationBinding: + """A class to represent an operation binding. + + Attributes: + amqp : AMQP operation binding (optional) + kafka : Kafka operation binding (optional) + sqs : SQS operation binding (optional) + nats : NATS operation binding (optional) + redis : Redis operation binding (optional) + http: HTTP operation binding (optional) + """ + + amqp: amqp_bindings.OperationBinding | None = None + kafka: kafka_bindings.OperationBinding | None = None + sqs: sqs_bindings.OperationBinding | None = None + nats: nats_bindings.OperationBinding | None = None + redis: redis_bindings.OperationBinding | None = None + http: http_bindings.OperationBinding | None = None diff --git a/faststream/specification/schema/bindings/nats.py b/faststream/specification/schema/bindings/nats.py new file mode 100644 index 0000000000..6911e74cd6 --- /dev/null +++ b/faststream/specification/schema/bindings/nats.py @@ -0,0 +1,31 @@ +"""AsyncAPI NATS bindings. + +References: https://github.com/asyncapi/bindings/tree/master/nats +""" + +from dataclasses import dataclass +from typing import Any + + +@dataclass +class ChannelBinding: + """A class to represent channel binding. + + Attributes: + subject : subject of the channel binding + queue : optional queue for the channel binding + """ + + subject: str + queue: str | None + + +@dataclass +class OperationBinding: + """A class to represent an operation binding. + + Attributes: + reply_to : optional dictionary containing reply information + """ + + reply_to: dict[str, Any] | None diff --git a/faststream/specification/schema/bindings/redis.py b/faststream/specification/schema/bindings/redis.py new file mode 100644 index 0000000000..4bdb41dcc6 --- /dev/null +++ b/faststream/specification/schema/bindings/redis.py @@ -0,0 +1,33 @@ +"""AsyncAPI Redis bindings. + +References: https://github.com/asyncapi/bindings/tree/master/redis +""" + +from dataclasses import dataclass +from typing import Any + + +@dataclass +class ChannelBinding: + """A class to represent channel binding. + + Attributes: + channel : the channel name + method : the method used for binding (ssubscribe, psubscribe, subscribe) + """ + + channel: str + method: str | None = None + group_name: str | None = None + consumer_name: str | None = None + + +@dataclass +class OperationBinding: + """A class to represent an operation binding. + + Attributes: + reply_to : optional dictionary containing reply information + """ + + reply_to: dict[str, Any] | None = None diff --git a/faststream/specification/schema/bindings/sqs.py b/faststream/specification/schema/bindings/sqs.py new file mode 100644 index 0000000000..3c381aab7d --- /dev/null +++ b/faststream/specification/schema/bindings/sqs.py @@ -0,0 +1,33 @@ +"""AsyncAPI SQS bindings. + +References: https://github.com/asyncapi/bindings/tree/master/sqs +""" + +from dataclasses import dataclass +from typing import Any + + +@dataclass +class ChannelBinding: + """A class to represent channel binding. + + Attributes: + queue : a dictionary representing the queue + bindingVersion : a string representing the binding version (default: "custom") + """ + + queue: dict[str, Any] + bindingVersion: str = "custom" + + +@dataclass +class OperationBinding: + """A class to represent an operation binding. + + Attributes: + replyTo : optional dictionary containing reply information + bindingVersion : version of the binding, default is "custom" + """ + + replyTo: dict[str, Any] | None = None + bindingVersion: str = "custom" diff --git a/faststream/specification/schema/broker.py b/faststream/specification/schema/broker.py new file mode 100644 index 0000000000..18c6246191 --- /dev/null +++ b/faststream/specification/schema/broker.py @@ -0,0 +1,17 @@ +from collections.abc import Iterable +from dataclasses import dataclass +from typing import TYPE_CHECKING, Optional, Union + +if TYPE_CHECKING: + from faststream.security import BaseSecurity + from faststream.specification.schema.extra import Tag, TagDict + + +@dataclass +class BrokerSpec: + url: list[str] + protocol: str | None + protocol_version: str | None + description: str | None + tags: Iterable[Union["Tag", "TagDict"]] + security: Optional["BaseSecurity"] 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..e9bffb3b67 --- /dev/null +++ b/faststream/specification/schema/extra/contact.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + +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: AnyHttpUrl | None = None + email: EmailStr | None = 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..c57e768497 --- /dev/null +++ b/faststream/specification/schema/extra/external_docs.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass + +from typing_extensions import Required, TypedDict + + +class ExternalDocsDict(TypedDict, total=False): + url: Required[str] + description: str + + +@dataclass +class ExternalDocs: + url: str + description: str | None = None diff --git a/faststream/specification/schema/extra/license.py b/faststream/specification/schema/extra/license.py new file mode 100644 index 0000000000..c28adf3acf --- /dev/null +++ b/faststream/specification/schema/extra/license.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + +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: AnyHttpUrl | None = None diff --git a/faststream/specification/schema/extra/tag.py b/faststream/specification/schema/extra/tag.py new file mode 100644 index 0000000000..ee5f1d8a30 --- /dev/null +++ b/faststream/specification/schema/extra/tag.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + +from typing_extensions import Required, TypedDict + +from .external_docs import ExternalDocs, ExternalDocsDict + + +class TagDict(TypedDict, total=False): + name: Required[str] + description: str + external_docs: ExternalDocs | ExternalDocsDict + + +@dataclass +class Tag: + name: str + description: str | None = None + external_docs: ExternalDocs | ExternalDocsDict | None = 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..fad0a1f800 --- /dev/null +++ b/faststream/specification/schema/message/model.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from faststream._internal.basic_types import AnyDict + + +@dataclass +class Message: + payload: AnyDict # JSON Schema + + title: str | 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..58f426dc17 --- /dev/null +++ b/faststream/specification/schema/operation/model.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from faststream.specification.schema.bindings import OperationBinding +from faststream.specification.schema.message import Message + + +@dataclass +class Operation: + message: Message + bindings: OperationBinding | None diff --git a/faststream/specification/schema/publisher.py b/faststream/specification/schema/publisher.py new file mode 100644 index 0000000000..6d89f2dab8 --- /dev/null +++ b/faststream/specification/schema/publisher.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + +from .bindings import ChannelBinding +from .operation import Operation + + +@dataclass +class PublisherSpec: + description: str | None + operation: Operation + bindings: ChannelBinding | None diff --git a/faststream/specification/schema/subscriber.py b/faststream/specification/schema/subscriber.py new file mode 100644 index 0000000000..de43471ca3 --- /dev/null +++ b/faststream/specification/schema/subscriber.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + +from .bindings import ChannelBinding +from .operation import Operation + + +@dataclass +class SubscriberSpec: + description: str | None + operation: Operation + bindings: ChannelBinding | None diff --git a/faststream/testing/__init__.py b/faststream/testing/__init__.py deleted file mode 100644 index f1a3c33c12..0000000000 --- a/faststream/testing/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from faststream.testing.app import TestApp - -__all__ = ("TestApp",) diff --git a/faststream/testing/app.py b/faststream/testing/app.py deleted file mode 100644 index c9fc4aa632..0000000000 --- a/faststream/testing/app.py +++ /dev/null @@ -1,72 +0,0 @@ -from contextlib import ExitStack -from functools import partial -from typing import TYPE_CHECKING, Any, Dict, Optional, Type, TypeVar - -from anyio.from_thread import start_blocking_portal - -from faststream.broker.core.usecase import BrokerUsecase - -if TYPE_CHECKING: - from types import TracebackType - - from faststream.app import FastStream - from faststream.types import SettingField - -Broker = TypeVar("Broker", bound=BrokerUsecase[Any, Any]) - - -class TestApp: - """A class to represent a test application.""" - - __test__ = False - - app: "FastStream" - _extra_options: Dict[str, "SettingField"] - - def __init__( - self, - app: "FastStream", - run_extra_options: Optional[Dict[str, "SettingField"]] = None, - ) -> None: - self.app = app - self._extra_options = run_extra_options or {} - - def __enter__(self) -> "FastStream": - with ExitStack() as stack: - portal = stack.enter_context(start_blocking_portal()) - - lifespan_context = self.app.lifespan_context(**self._extra_options) - stack.enter_context(portal.wrap_async_context_manager(lifespan_context)) - portal.call(partial(self.app.start, **self._extra_options)) - - @stack.callback - def wait_shutdown() -> None: - portal.call(self.app.stop) - - self.exit_stack = stack.pop_all() - - return self.app - - def __exit__( - self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> None: - self.exit_stack.close() - - async def __aenter__(self) -> "FastStream": - self.lifespan_scope = self.app.lifespan_context(**self._extra_options) - await self.lifespan_scope.__aenter__() - await self.app.start(**self._extra_options) - return self.app - - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> None: - """Exit the asynchronous context manager.""" - await self.app.stop() - await self.lifespan_scope.__aexit__(exc_type, exc_val, exc_tb) diff --git a/faststream/testing/broker.py b/faststream/testing/broker.py deleted file mode 100644 index d27eacf1f0..0000000000 --- a/faststream/testing/broker.py +++ /dev/null @@ -1,200 +0,0 @@ -import warnings -from abc import abstractmethod -from contextlib import asynccontextmanager, contextmanager -from functools import partial -from typing import ( - TYPE_CHECKING, - Any, - AsyncGenerator, - Generator, - Generic, - List, - Optional, - Tuple, - Type, - TypeVar, -) -from unittest import mock -from unittest.mock import MagicMock - -from faststream.broker.core.usecase import BrokerUsecase -from faststream.testing.app import TestApp -from faststream.utils.ast import is_contains_context_name -from faststream.utils.functions import sync_fake_context - -if TYPE_CHECKING: - from types import TracebackType - - from faststream.broker.subscriber.proto import SubscriberProto - - -Broker = TypeVar("Broker", bound=BrokerUsecase[Any, Any]) - - -class TestBroker(Generic[Broker]): - """A class to represent a test broker.""" - - # This is set so pytest ignores this class - __test__ = False - - def __init__( - self, - broker: Broker, - with_real: bool = False, - connect_only: Optional[bool] = None, - ) -> None: - self.with_real = with_real - self.broker = broker - - if connect_only is None: - try: - connect_only = is_contains_context_name( - self.__class__.__name__, - TestApp.__name__, - ) - except Exception: # pragma: no cover - warnings.warn( - ( - "\nError `{e!r}` occurred at `{self.__class__.__name__}` AST parsing." - "\n`connect_only` is set to `False` by default." - ), - category=RuntimeWarning, - stacklevel=1, - ) - - connect_only = False - - self.connect_only = connect_only - self._fake_subscribers: List[SubscriberProto[Any]] = [] - - async def __aenter__(self) -> Broker: - self._ctx = self._create_ctx() - return await self._ctx.__aenter__() - - async def __aexit__(self, *args: Any) -> None: - await self._ctx.__aexit__(*args) - - @asynccontextmanager - async def _create_ctx(self) -> AsyncGenerator[Broker, None]: - if self.with_real: - self._fake_start(self.broker) - context = sync_fake_context() - else: - context = self._patch_broker(self.broker) - - with context: - async with self.broker: - try: - if not self.connect_only: - await self.broker.start() - yield self.broker - finally: - self._fake_close(self.broker) - - @contextmanager - def _patch_broker(self, broker: Broker) -> Generator[None, None, None]: - with mock.patch.object( - broker, - "start", - wraps=partial(self._fake_start, broker), - ), mock.patch.object( - broker, - "_connect", - wraps=partial(self._fake_connect, broker), - ), mock.patch.object( - broker, - "close", - ), mock.patch.object( - broker, - "_connection", - new=None, - ), mock.patch.object( - broker, - "_producer", - new=None, - ), mock.patch.object( - broker, - "ping", - return_value=True, - ): - yield - - def _fake_start(self, broker: Broker, *args: Any, **kwargs: Any) -> None: - broker.setup() - - patch_broker_calls(broker) - - for p in broker._publishers.values(): - if getattr(p, "_fake_handler", None): - continue - - sub, is_real = self.create_publisher_fake_subscriber(broker, p) - - if not is_real: - self._fake_subscribers.append(sub) - - if not sub.calls: - - @sub - async def publisher_response_subscriber(msg: Any) -> None: - pass - - broker.setup_subscriber(sub) - - if is_real: - mock = MagicMock() - p.set_test(mock=mock, with_fake=False) # type: ignore[attr-defined] - for h in sub.calls: - h.handler.set_test() - assert h.handler.mock # nosec B101 - h.handler.mock.side_effect = mock - - else: - handler = sub.calls[0].handler - handler.set_test() - assert handler.mock # nosec B101 - p.set_test(mock=handler.mock, with_fake=True) # type: ignore[attr-defined] - - for subscriber in broker._subscribers.values(): - subscriber.running = True - - def _fake_close( - self, - broker: Broker, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional["TracebackType"] = None, - ) -> None: - for p in broker._publishers.values(): - if getattr(p, "_fake_handler", None): - p.reset_test() # type: ignore[attr-defined] - - for sub in self._fake_subscribers: - self.broker._subscribers.pop(hash(sub), None) # type: ignore[attr-defined] - self._fake_subscribers = [] - - for h in broker._subscribers.values(): - h.running = False - for call in h.calls: - call.handler.reset_test() - - @staticmethod - @abstractmethod - def create_publisher_fake_subscriber( - broker: Broker, publisher: Any - ) -> Tuple["SubscriberProto[Any]", bool]: - raise NotImplementedError - - @staticmethod - @abstractmethod - async def _fake_connect(broker: Broker, *args: Any, **kwargs: Any) -> None: - raise NotImplementedError - - -def patch_broker_calls(broker: "BrokerUsecase[Any, Any]") -> None: - """Patch broker calls.""" - broker._abc_start() - - for handler in broker._subscribers.values(): - for h in handler.calls: - h.handler.set_test() diff --git a/faststream/types.py b/faststream/types.py deleted file mode 100644 index ae34858025..0000000000 --- a/faststream/types.py +++ /dev/null @@ -1,120 +0,0 @@ -from datetime import datetime -from decimal import Decimal -from typing import ( - Any, - AsyncContextManager, - Awaitable, - Callable, - ClassVar, - Dict, - List, - Mapping, - Optional, - Protocol, - Sequence, - TypeVar, - Union, -) - -from typing_extensions import ParamSpec, TypeAlias - -AnyDict: TypeAlias = Dict[str, Any] -AnyHttpUrl: TypeAlias = str - -F_Return = TypeVar("F_Return") -F_Spec = ParamSpec("F_Spec") - -AnyCallable: TypeAlias = Callable[..., Any] -NoneCallable: TypeAlias = Callable[..., None] -AsyncFunc: TypeAlias = Callable[..., Awaitable[Any]] -AsyncFuncAny: TypeAlias = Callable[[Any], Awaitable[Any]] - -DecoratedCallable: TypeAlias = AnyCallable -DecoratedCallableNone: TypeAlias = NoneCallable - -Decorator: TypeAlias = Callable[[AnyCallable], AnyCallable] - -JsonArray: TypeAlias = Sequence["DecodedMessage"] - -JsonTable: TypeAlias = Dict[str, "DecodedMessage"] - -JsonDecodable: TypeAlias = Union[ - bool, - bytes, - bytearray, - float, - int, - str, - None, -] - -DecodedMessage: TypeAlias = Union[ - JsonDecodable, - JsonArray, - JsonTable, -] - -SendableArray: TypeAlias = Sequence["BaseSendableMessage"] - -SendableTable: TypeAlias = Dict[str, "BaseSendableMessage"] - - -class StandardDataclass(Protocol): - """Protocol to check type is dataclass.""" - - __dataclass_fields__: ClassVar[Dict[str, Any]] - - -BaseSendableMessage: TypeAlias = Union[ - JsonDecodable, - Decimal, - datetime, - StandardDataclass, - SendableTable, - SendableArray, - None, -] - -try: - from faststream._compat import BaseModel - - SendableMessage: TypeAlias = Union[ - BaseModel, - BaseSendableMessage, - ] - -except ImportError: - SendableMessage: TypeAlias = BaseSendableMessage # type: ignore[no-redef,misc] - -SettingField: TypeAlias = Union[ - bool, - str, - List[Union[bool, str]], - List[str], - List[bool], -] - -Lifespan: TypeAlias = Callable[..., AsyncContextManager[None]] - - -class LoggerProto(Protocol): - def log( - self, - level: int, - msg: Any, - /, - *, - exc_info: Any = None, - extra: Optional[Mapping[str, Any]] = None, - ) -> None: ... - - -class _EmptyPlaceholder: - def __repr__(self) -> str: - return "EMPTY" - - def __eq__(self, other: object) -> bool: - return isinstance(other, _EmptyPlaceholder) - - -EMPTY: Any = _EmptyPlaceholder() diff --git a/faststream/utils/__init__.py b/faststream/utils/__init__.py deleted file mode 100644 index 18f6b4c7f5..0000000000 --- a/faststream/utils/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from fast_depends import Depends -from fast_depends import inject as apply_types - -from faststream.utils.context import Context, ContextRepo, Header, Path, context -from faststream.utils.no_cast import NoCast - -__all__ = ( - "Context", - "ContextRepo", - "Depends", - "Header", - "NoCast", - "Path", - "apply_types", - "context", -) diff --git a/faststream/utils/ast.py b/faststream/utils/ast.py deleted file mode 100644 index 5453d621f1..0000000000 --- a/faststream/utils/ast.py +++ /dev/null @@ -1,53 +0,0 @@ -import ast -import traceback -from functools import lru_cache -from pathlib import Path -from typing import Iterator, List, Optional, Union, cast - - -def is_contains_context_name(scip_name: str, name: str) -> bool: - stack = traceback.extract_stack()[-3] - tree = read_source_ast(stack.filename) - node = cast("Union[ast.With, ast.AsyncWith]", find_ast_node(tree, stack.lineno)) - context_calls = get_withitem_calls(node) - - try: - pos = context_calls.index(scip_name) - except ValueError: - pos = 1 - - return name in context_calls[pos:] - - -@lru_cache -def read_source_ast(filename: str) -> ast.Module: - return ast.parse(Path(filename).read_text()) - - -def find_ast_node(module: ast.Module, lineno: Optional[int]) -> Optional[ast.AST]: - if lineno is not None: # pragma: no branch - for i in getattr(module, "body", ()): - if i.lineno == lineno: - return cast("ast.AST", i) - - r = find_ast_node(i, lineno) - if r is not None: - return r - - return None - - -def find_withitems(node: Union[ast.With, ast.AsyncWith]) -> Iterator[ast.withitem]: - if isinstance(node, (ast.With, ast.AsyncWith)): - yield from node.items - - for i in getattr(node, "body", ()): - yield from find_withitems(i) - - -def get_withitem_calls(node: Union[ast.With, ast.AsyncWith]) -> List[str]: - return [ - id - for i in find_withitems(node) - if (id := getattr(i.context_expr.func, "id", None)) # type: ignore[attr-defined] - ] diff --git a/faststream/utils/classes.py b/faststream/utils/classes.py deleted file mode 100644 index 1bf053cbce..0000000000 --- a/faststream/utils/classes.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Any, ClassVar, Optional - -from typing_extensions import Self - - -class Singleton: - """A class to implement the Singleton design pattern. - - Attributes: - _instance : the single instance of the class - - Methods: - __new__ : creates a new instance of the class if it doesn't exist, otherwise returns the existing instance - _drop : sets the instance to None, allowing a new instance to be created - """ - - _instance: ClassVar[Optional[Self]] = None - - def __new__(cls, *args: Any, **kwargs: Any) -> Self: - """Create a singleton instance of a class. - - Args: - *args: Variable length argument list - **kwargs: Arbitrary keyword arguments - - Returns: - The singleton instance of the class - """ - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - - @classmethod - def _drop(cls) -> None: - """Drop the instance of a class. - - Returns: - None - """ - cls._instance = None diff --git a/faststream/utils/context/__init__.py b/faststream/utils/context/__init__.py deleted file mode 100644 index 054ce3f1a5..0000000000 --- a/faststream/utils/context/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from faststream.utils.context.builders import Context, Header, Path -from faststream.utils.context.repository import ContextRepo, context - -__all__ = ( - "Context", - "ContextRepo", - "Header", - "Path", - "context", -) diff --git a/faststream/utils/context/builders.py b/faststream/utils/context/builders.py deleted file mode 100644 index 76e7499ba3..0000000000 --- a/faststream/utils/context/builders.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Any, Callable, Optional - -from faststream.types import EMPTY -from faststream.utils.context.types import Context as Context_ - - -def Context( # noqa: N802 - real_name: str = "", - *, - cast: bool = False, - default: Any = EMPTY, - initial: Optional[Callable[..., Any]] = None, -) -> Any: - return Context_( - real_name=real_name, - cast=cast, - default=default, - initial=initial, - ) - - -def Header( # noqa: N802 - real_name: str = "", - *, - cast: bool = True, - default: Any = EMPTY, -) -> Any: - return Context_( - real_name=real_name, - cast=cast, - default=default, - prefix="message.headers.", - ) - - -def Path( # noqa: N802 - real_name: str = "", - *, - cast: bool = True, - default: Any = EMPTY, -) -> Any: - return Context_( - real_name=real_name, - cast=cast, - default=default, - prefix="message.path.", - ) diff --git a/faststream/utils/context/types.py b/faststream/utils/context/types.py deleted file mode 100644 index 5ca17d7ff3..0000000000 --- a/faststream/utils/context/types.py +++ /dev/null @@ -1,96 +0,0 @@ -from typing import Any, Callable, Optional - -from fast_depends.library import CustomField - -from faststream.types import EMPTY, AnyDict -from faststream.utils.context.repository import context - - -class Context(CustomField): - """A class to represent a context. - - Attributes: - param_name : name of the parameter - - Methods: - __init__ : constructor method - use : method to use the context - """ - - param_name: str - - def __init__( - self, - real_name: str = "", - *, - default: Any = EMPTY, - initial: Optional[Callable[..., Any]] = None, - cast: bool = False, - prefix: str = "", - ) -> None: - """Initialize the object. - - Args: - real_name: The real name of the object. - default: The default value of the object. - initial: The initial value builder. - cast: Whether to cast the object. - prefix: The prefix to be added to the name of the object. - - Raises: - TypeError: If the default value is not provided. - """ - self.name = real_name - self.default = default - self.prefix = prefix - self.initial = initial - super().__init__( - cast=cast, - required=(default is EMPTY), - ) - - def use(self, /, **kwargs: Any) -> AnyDict: - """Use the given keyword arguments. - - Args: - **kwargs: Keyword arguments to be used - - Returns: - A dictionary containing the updated keyword arguments - """ - name = f"{self.prefix}{self.name or self.param_name}" - - if EMPTY != ( # noqa: SIM300 - v := resolve_context_by_name( - name=name, - default=self.default, - initial=self.initial, - ) - ): - kwargs[self.param_name] = v - - else: - kwargs.pop(self.param_name, None) - - return kwargs - - -def resolve_context_by_name( - name: str, - default: Any, - initial: Optional[Callable[..., Any]], -) -> Any: - value: Any = EMPTY - - try: - value = context.resolve(name) - - except (KeyError, AttributeError): - if EMPTY != default: # noqa: SIM300 - value = default - - elif initial is not None: - value = initial() - context.set_global(name, value) - - return value diff --git a/faststream/utils/data.py b/faststream/utils/data.py deleted file mode 100644 index cf00f649ef..0000000000 --- a/faststream/utils/data.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Type, TypeVar - -from faststream.types import AnyDict - -TypedDictCls = TypeVar("TypedDictCls") - - -def filter_by_dict(typed_dict: Type[TypedDictCls], data: AnyDict) -> TypedDictCls: - """Filter a dictionary based on a typed dictionary. - - Args: - typed_dict: The typed dictionary to filter by. - data: The dictionary to filter. - - Returns: - A new instance of the typed dictionary with only the keys present in the data dictionary. - """ - annotations = typed_dict.__annotations__ - return typed_dict( # type: ignore - {k: v for k, v in data.items() if k in annotations} - ) diff --git a/faststream/utils/functions.py b/faststream/utils/functions.py deleted file mode 100644 index 5e4ce4b2c8..0000000000 --- a/faststream/utils/functions.py +++ /dev/null @@ -1,86 +0,0 @@ -from contextlib import asynccontextmanager, contextmanager -from functools import wraps -from typing import ( - Any, - AsyncIterator, - Awaitable, - Callable, - ContextManager, - Iterator, - Optional, - Union, - overload, -) - -import anyio -from fast_depends.core import CallModel -from fast_depends.utils import run_async as call_or_await - -from faststream.types import F_Return, F_Spec - -__all__ = ( - "call_or_await", - "drop_response_type", - "fake_context", - "timeout_scope", - "to_async", -) - - -@overload -def to_async( - func: Callable[F_Spec, Awaitable[F_Return]], -) -> Callable[F_Spec, Awaitable[F_Return]]: ... - - -@overload -def to_async( - func: Callable[F_Spec, F_Return], -) -> Callable[F_Spec, Awaitable[F_Return]]: ... - - -def to_async( - func: Union[ - Callable[F_Spec, F_Return], - Callable[F_Spec, Awaitable[F_Return]], - ], -) -> Callable[F_Spec, Awaitable[F_Return]]: - """Converts a synchronous function to an asynchronous function.""" - - @wraps(func) - async def to_async_wrapper(*args: F_Spec.args, **kwargs: F_Spec.kwargs) -> F_Return: - """Wraps a function to make it asynchronous.""" - return await call_or_await(func, *args, **kwargs) - - return to_async_wrapper - - -def timeout_scope( - timeout: Optional[float] = 30, - raise_timeout: bool = False, -) -> ContextManager[anyio.CancelScope]: - scope: Callable[[Optional[float]], ContextManager[anyio.CancelScope]] - scope = anyio.fail_after if raise_timeout else anyio.move_on_after - - return scope(timeout) - - -@asynccontextmanager -async def fake_context(*args: Any, **kwargs: Any) -> AsyncIterator[None]: - yield None - - -@contextmanager -def sync_fake_context(*args: Any, **kwargs: Any) -> Iterator[None]: - yield None - - -def drop_response_type( - model: CallModel[F_Spec, F_Return], -) -> CallModel[F_Spec, F_Return]: - model.response_model = None - return model - - -async def return_input(x: Any) -> Any: - return x diff --git a/faststream/utils/no_cast.py b/faststream/utils/no_cast.py deleted file mode 100644 index 6a96fbd029..0000000000 --- a/faststream/utils/no_cast.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Any - -from fast_depends.library import CustomField - -from faststream.types import AnyDict - - -class NoCast(CustomField): - """A class that represents a custom field without casting. - - You can use it to annotate fields, that should not be casted. - - Usage: - - `data: Annotated[..., NoCast()]` - """ - - def __init__(self) -> None: - super().__init__(cast=False) - - def use(self, **kwargs: Any) -> AnyDict: - return kwargs diff --git a/justfile b/justfile new file mode 100644 index 0000000000..5ee68a5176 --- /dev/null +++ b/justfile @@ -0,0 +1,183 @@ +[doc("All command information")] +default: + @just --list --unsorted --list-heading $'FastStream commands…\n' + + +# Infra +[doc("Init infra")] +[group("infra")] +init python="3.10": + docker build . --build-arg PYTHON_VERSION={{python}} + +[doc("Run all containers")] +[group("infra")] +up: + docker compose up -d + +[doc("Stop all containers")] +[group("infra")] +stop: + docker compose stop + +[doc("Down all containers")] +[group("infra")] +down: + docker compose down + + +[doc("Run fast tests")] +[group("tests")] +test path="tests/" params="" marks="not slow and not kafka and not confluent and not redis and not rabbit and not nats": + docker compose exec faststream uv run pytest {{path}} -m "{{marks}}" {{params}} + +[doc("Run all tests")] +[group("tests")] +test-all path="tests/" params="" marks="all": + docker compose exec faststream uv run pytest {{path}} -m "{{marks}}" {{params}} + +[doc("Run fast tests with coverage")] +[group("tests")] +coverage-test path="tests/" params="" marks="not slow and not kafka and not confluent and not redis and not rabbit and not nats": + -docker compose exec faststream uv run sh -c "coverage run -m pytest {{path}} -m '{{marks}}' {{params}} && coverage combine && coverage report --show-missing --skip-covered --sort=cover --precision=2 && rm .coverage*" + +[doc("Run all tests with coverage")] +[group("tests")] +coverage-test-all path="tests/" params="" marks="all": + -docker compose exec faststream uv run sh -c "coverage run -m pytest {{path}} -m '{{marks}}' {{params}} && coverage combine && coverage report --show-missing --skip-covered --sort=cover --precision=2 && rm .coverage*" + + +# Docs +[doc("Build docs")] +[group("docs")] +docs-build: + docker compose exec -T faststream uv run sh -c "cd docs && python docs.py build" + +[doc("Serve docs")] +[group("docs")] +docs-serve: + docker compose exec faststream uv run sh -c "cd docs && python docs.py live 8000 --fast" + + +# Linter +[doc("Ruff check")] +[group("linter")] +ruff-check: + -docker compose exec -T faststream uv run ruff check --exit-non-zero-on-fix + +[doc("Ruff format")] +[group("linter")] +ruff-format: + -docker compose exec -T faststream uv run ruff format + +[doc("Codespell check")] +[group("linter")] +codespell: + -docker compose exec -T faststream uv run codespell + +[doc("Linter run")] +[group("linter")] +linter: ruff-check ruff-format codespell + + +# Static analysis +[doc("Mypy check")] +[group("static analysis")] +mypy: + -docker compose exec -T faststream uv run mypy + +[doc("Bandit check")] +[group("static analysis")] +bandit: + -docker compose exec -T faststream uv run bandit -c pyproject.toml -r faststream + +[doc("Semgrep check")] +[group("static analysis")] +semgrep: + -docker compose exec -T faststream uv run semgrep scan --config auto --error + +[doc("Static analysis check")] +[group("static analysis")] +static-analysis: mypy bandit semgrep + +# Kafka +[doc("Run kafka container")] +[group("kafka")] +kafka-up: + docker compose up -d kafka + +[doc("Stop kafka container")] +[group("kafka")] +kafka-stop: + docker compose stop kafka + +[doc("Show kafka logs")] +[group("kafka")] +kafka-logs: + docker compose logs -f kafka + +[doc("Run kafka tests")] +[group("kafka")] +kafka-tests: (test "kafka") + + +# RabbitMQ +[doc("Run rabbitmq container")] +[group("rabbitmq")] +rabbit-up: + docker compose up -d rabbitmq + +[doc("Stop rabbitmq container")] +[group("rabbitmq")] +rabbit-stop: + docker compose stop rabbitmq + +[doc("Show rabbitmq logs")] +[group("rabbitmq")] +rabbit-logs: + docker compose logs -f rabbitmq + +[doc("Run rabbitmq tests")] +[group("rabbitmq")] +rabbit-tests: (test "rabbit") + + +# Redis +[doc("Run redis container")] +[group("redis")] +redis-up: + docker compose up -d redis + +[doc("Stop redis container")] +[group("redis")] +redis-stop: + docker compose stop redis + +[doc("Show redis logs")] +[group("redis")] +redis-logs: + docker compose logs -f redis + +[doc("Run redis tests")] +[group("redis")] +redis-tests: (test "redis") + + +# Nats +[doc("Run nats container")] +[group("nats")] +nats-up: + docker compose up -d nats + +[doc("Stop nats container")] +[group("nats")] +nats-stop: + docker compose stop nats + +[doc("Show nats logs")] +[group("nats")] +nats-logs: + docker compose logs -f nats + +[doc("Run nats tests")] +[group("nats")] +nats-tests: (test "nats") diff --git a/pyproject.toml b/pyproject.toml index 92dc3bcf74..16a7024d51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "FastStream: the simplest way to work with a messaging queues" readme = "README.md" authors = [ { name = "AG2AI", email = "support@ag2.ai" }, - { name = "Nikita Pastukhov", email = "diementros@yandex.com" }, + { name = "Nikita Pastukhov", email = "nikita@pastukhov-dev.com" }, ] keywords = [ @@ -20,7 +20,7 @@ keywords = [ "message brokers", ] -requires-python = ">=3.8" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -29,12 +29,11 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", @@ -57,12 +56,11 @@ dynamic = ["version"] dependencies = [ "anyio>=3.7.1,<5", - "fast-depends>=2.4.0b0,<3.0.0", + "fast-depends[pydantic]>=3.0.0a7,<4.0.0", "typing-extensions>=4.8.0", ] [project.optional-dependencies] -# public distributions rabbit = ["aio-pika>=9,<10"] kafka = ["aiokafka>=0.9,<0.13"] @@ -85,28 +83,30 @@ cli = [ prometheus = ["prometheus-client>=0.20.0,<0.30.0"] -# dev dependencies +[dependency-groups] optionals = ["faststream[rabbit,kafka,confluent,nats,redis,otel,cli,prometheus]"] -devdocs = [ +docs = [ "mkdocs-material==9.6.14", "mkdocs-static-i18n==1.3.0", "mdx-include==1.4.2", - "mkdocstrings[python]==0.29.1; python_version >= '3.9'", - "mkdocstrings[python]==0.26.1; python_version < '3.9'", + "mkdocstrings[python]==0.29.1", "mkdocs-literate-nav==0.6.2", "mkdocs-git-revision-date-localized-plugin==1.4.7", "mike==2.1.3", # versioning "mkdocs-minify-plugin==0.8.0", "mkdocs-macros-plugin==1.3.7", # includes with variables "mkdocs-glightbox==0.4.0", # img zoom - "pillow", # required for mkdocs-glightbo - "cairosvg", # required for mkdocs-glightbo - "requests", # using in CI, do not pin it + "pillow", # required for mkdocs-glightbox + "cairosvg", # required for mkdocs-glightbox + "httpx==0.28.1", # using in CI ] -types = [ - "faststream[optionals]", +lint = [ + "ruff==0.11.13", + "bandit==1.8.3", + "semgrep==1.125.0", + "codespell==2.4.1", "mypy==1.16.0", # mypy extensions "types-Deprecated", @@ -120,43 +120,34 @@ types = [ "confluent-kafka-stubs; python_version >= '3.11'", ] -lint = [ - "faststream[types]", - "ruff==0.11.13", - "bandit==1.8.3; python_version >= '3.9'", - "bandit==1.7.10; python_version < '3.9'", - "semgrep==1.125.0; python_version >= '3.9'", - "semgrep==1.99.0; python_version < '3.9'", - "codespell==2.4.1", -] - test-core = [ - "coverage[toml]==7.9.1; python_version >= '3.9'", - "coverage[toml]==7.6.1; python_version < '3.9'", - "pytest==8.4.0; python_version >= '3.9'", - "pytest==8.3.5; python_version < '3.9'", - "pytest-asyncio==1.0.0; python_version >= '3.9'", - "pytest-asyncio==0.24.0; python_version < '3.9'", + "coverage[toml]==7.9.1", + "pytest==8.4.0", + "pytest-asyncio==1.0.0", + "pytest-retry==1.7.0", "dirty-equals==0.9.0", - "typing-extensions>=4.8.0,<4.12.1; python_version < '3.9'", # to fix dirty-equals + "pytest-timeout>=2.4.0", + "httpx==0.28.1", ] testing = [ - "faststream[test-core]", + {include-group = "test-core"}, "fastapi==0.115.12", "pydantic-settings>=2.0.0,<3.0.0", - "httpx==0.28.1", "PyYAML==6.0.2", "email-validator==2.2.0", + "msgspec", "uvicorn>=0.34.3; python_version >= '3.9'", "uvicorn==0.33.0; python_version < '3.9'", "psutil==7.0.0", ] dev = [ - "faststream[optionals,lint,testing,devdocs]", - "pre-commit==3.5.0; python_version < '3.9'", - "pre-commit==4.2.0; python_version >= '3.9'", + {include-group = "optionals"}, + {include-group = "lint"}, + {include-group = "testing"}, + {include-group = "docs"}, + "pre-commit==4.2.0", "detect-secrets==1.5.0", ] @@ -180,12 +171,10 @@ exclude = ["/tests", "/docs"] [tool.mypy] files = ["faststream", "tests/mypy"] strict = true +python_version = "3.10" strict_bytes = true local_partial_types = true -python_version = "3.8" ignore_missing_imports = true -install_types = true -non_interactive = true plugins = ["pydantic.mypy"] # from https://blog.wolt.com/engineering/2021/09/30/professional-grade-mypy-configuration/ @@ -200,114 +189,9 @@ disallow_incomplete_defs = true disallow_untyped_decorators = true disallow_any_unimported = false -[tool.ruff] -fix = true -line-length = 88 -target-version = "py38" -include = [ - "faststream/**/*.py", - "faststream/**/*.pyi", - "tests/**/*.py", - "docs/**/*.py", - "pyproject.toml", -] -exclude = ["docs/docs_src"] - -[tool.ruff.lint] -select = [ - "E", # pycodestyle errors https://docs.astral.sh/ruff/rules/#error-e - "W", # pycodestyle warnings https://docs.astral.sh/ruff/rules/#warning-w - "C90", # mccabe https://docs.astral.sh/ruff/rules/#mccabe-c90 - "N", # pep8-naming https://docs.astral.sh/ruff/rules/#pep8-naming-n - "D", # pydocstyle https://docs.astral.sh/ruff/rules/#pydocstyle-d - "I", # isort https://docs.astral.sh/ruff/rules/#isort-i - "F", # pyflakes https://docs.astral.sh/ruff/rules/#pyflakes-f - "ASYNC", # flake8-async https://docs.astral.sh/ruff/rules/#flake8-async-async - "C4", # flake8-comprehensions https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 - "B", # flake8-bugbear https://docs.astral.sh/ruff/rules/#flake8-bugbear-b - "Q", # flake8-quotes https://docs.astral.sh/ruff/rules/#flake8-quotes-q - "T20", # flake8-print https://docs.astral.sh/ruff/rules/#flake8-print-t20 - "SIM", # flake8-simplify https://docs.astral.sh/ruff/rules/#flake8-simplify-sim - "PT", # flake8-pytest-style https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - "PTH", # flake8-use-pathlib https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth - "TCH", # flake8-type-checking https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch - "RUF", # Ruff-specific rules https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf - "PERF", # Perflint https://docs.astral.sh/ruff/rules/#perflint-perf - "UP", # pyupgrade https://docs.astral.sh/ruff/rules/#pyupgrade-up -] - -ignore = [ - "ASYNC109", # own timeout implementation - - "E501", # line too long, handled by formatter later - "C901", # too complex - - # todo pep8-naming - "N817", # CamelCase `*` imported as acronym `*` - "N815", # Variable `*` in class scope should not be mixedCase - "N803", # Argument name `expandMessageExamples` should be lowercase - - # todo pydocstyle - "D100", # missing docstring in public module - "D101", - "D102", - "D103", - "D104", # missing docstring in public package - "D105", # missing docstring in magic methods - "D106", # missing docstring in public nested class - "D107", # missing docstring in __init__ -] - -[tool.ruff.lint.per-file-ignores] -"tests/**" = [ - "D101", # docstrings - "D102", - "D103", - "PLR2004", # magic-value-comparison - "S101", # use assert -] - -"docs/*.py" = [ - "D101", # docstrings - "D102", - "D103", -] - - -[tool.ruff.lint.isort] -case-sensitive = true - -[tool.ruff.format] -docstring-code-format = true - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.flake8-bugbear] -extend-immutable-calls = [ - "faststream.Depends", - "faststream.Context", - "faststream.broker.fastapi.context.Context", - "faststream.Header", - "faststream.Path", - "faststream.utils.Header", - "faststream.utils.Path", - "faststream.utils.Depends", - "faststream.utils.Context", - "faststream.utils.context.Depends", - "faststream.utils.context.Context", - "typer.Argument", - "typer.Option", - "pydantic.Field", - "rocketry.args.Arg", - "fastapi.Depends", - "fastapi.Header", - "fastapi.datastructures.Default", - "kafka.partitioner.default.DefaultPartitioner", -] - [tool.pytest.ini_options] minversion = "7.0" +timeout = 30 addopts = "-q -m 'not slow'" testpaths = ["tests"] markers = ["rabbit", "kafka", "confluent", "nats", "redis", "slow", "all"] diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000000..47f185f170 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,167 @@ +# Configuration file example: https://docs.astral.sh/ruff/configuration/ +# All settings: https://docs.astral.sh/ruff/settings/ + +fix = true +preview = true + +line-length = 88 +indent-width = 4 + +target-version = "py310" + +include = [ + "faststream/**/*.py", + "faststream/**/*.pyi", + "tests/**/*.py", + "docs/**/*.py", + "pyproject.toml", +] + +exclude = [ + "docs/docs_src" +] + +[format] +quote-style = "double" +indent-style = "space" +docstring-code-format = false + +[lint] +select = [ + "ALL", +] + +ignore = [ + "A", + "FA", + "TD", + "FIX", + "SLOT", + "ARG", + "EXE", + + "ASYNC109", + "ANN401", + "COM812", + "ISC001", + "TRY301", + "S101", + "SLF001", + "PLR0911", + "PLR0912", + "PLR0913", + "PLR2004", + "PYI036", + "PYI051", + "G004", + + "E501", # line too long, handled by formatter later + "C901", # too complex + + # preview + "CPY", + "PLC0415", + "PLC2701", # FIXME + "PLC2801", + "PLR6301", + "PLW1641", + "RUF029", + + # pep8-naming + "N817", # CamelCase `*` imported as acronym `*` + + # FIXME pydocstyle + "D100", # missing docstring in public module + "D101", + "D102", + "D103", + "D104", # missing docstring in public package + "D105", # missing docstring in magic methods + "D106", # missing docstring in public nested class + "D107", # missing docstring in __init__ + "DOC201", + "DOC202", + "DOC402", + "DOC501", + "DOC502", + + "FBT", # FIXME + "PLW2901", # FIXME + "BLE001", # FIXME + "S110", # FIXME + "PLR0917" # FIXME +] + +[lint.per-file-ignores] +"faststream/specification/**/*.py" = [ + "ERA001", + "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 +] + +"**/_compat.py" = [ + "PYI063", + "PLW3201", +] + +"tests/**/*.py" = [ + "ANN", # FIXME + "S", + "PLR0904", + "PT030", # FIXME + "PLR0914", + "PLC1901", + "RUF045", +] + +"docs/*.py" = [ + "ALL", # FIXME +] + +[lint.isort] +case-sensitive = true +combine-as-imports = true +force-wrap-aliases = true + +[lint.pydocstyle] +convention = "google" +ignore-decorators = ["typing.overload"] + +[lint.flake8-bugbear] +extend-immutable-calls = [ + "faststream.Header", + "faststream.Path", + "faststream.Depends", + "faststream.Context", + "faststream.Depends", + "faststream.params.Header", + "faststream.params.Path", + "faststream.params.Depends", + "faststream.params.Context", + "faststream.params.Depends", + "faststream._internal.fastapi.context.Context", + "typer.Argument", + "typer.Option", + "pydantic.Field", + "rocketry.args.Arg", + "fastapi.Depends", + "fastapi.Header", + "fastapi.datastructures.Default", + "kafka.partitioner.default.DefaultPartitioner", +] + +[lint.flake8-pytest-style] +fixture-parentheses = true +mark-parentheses = true +parametrize-names-type = "tuple" +parametrize-values-type = "tuple" +parametrize-values-row-type = "tuple" diff --git a/scripts/build-docs-pre-commit.sh b/scripts/build-docs-pre-commit.sh index fba2388ec4..1efef7430b 100755 --- a/scripts/build-docs-pre-commit.sh +++ b/scripts/build-docs-pre-commit.sh @@ -15,7 +15,7 @@ cd "$(dirname "$0")"/.. # In my case, I need to use a custom index URL. # Avoid pip spending time quietly retrying since # likely cause of failure is lack of VPN connection. -pip install --editable ".[dev]" \ +pip install --group dev --editable . \ --retries 1 \ --no-input \ --quiet diff --git a/scripts/lint-pre-commit.sh b/scripts/lint-pre-commit.sh index be747ccd9f..98fc3e0308 100755 --- a/scripts/lint-pre-commit.sh +++ b/scripts/lint-pre-commit.sh @@ -16,7 +16,7 @@ cd "$(dirname "$0")"/.. # Avoid pip spending time quietly retrying since # likely cause of failure is lack of VPN connection. pip install uv -uv pip install --editable ".[dev]" --quiet +uv pip install --group dev --editable . --quiet # Run on all files, # ignoring the paths passed to this script, diff --git a/scripts/publish.sh b/scripts/publish.sh deleted file mode 100755 index bc704d5e8a..0000000000 --- a/scripts/publish.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -hatch clean -hatch build -hatch publish diff --git a/scripts/serve-docs.sh b/scripts/serve-docs.sh deleted file mode 100755 index 5cd16f63d6..0000000000 --- a/scripts/serve-docs.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -x - -cd docs; python docs.py live "$@" diff --git a/scripts/start_test_env.sh b/scripts/start_test_env.sh deleted file mode 100755 index 906556db41..0000000000 --- a/scripts/start_test_env.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -source ./scripts/set_variables.sh - -docker compose -p $DOCKER_COMPOSE_PROJECT -f docs/includes/docker-compose.yaml up -d --no-recreate diff --git a/scripts/static-pre-commit.sh b/scripts/static-pre-commit.sh index 5beadb51c2..ee6f6473e4 100755 --- a/scripts/static-pre-commit.sh +++ b/scripts/static-pre-commit.sh @@ -16,7 +16,7 @@ cd "$(dirname "$0")"/.. # Avoid pip spending time quietly retrying since # likely cause of failure is lack of VPN connection. pip install uv -uv pip install --editable ".[dev]" --quiet +uv pip install --group dev --editable . --quiet # Run on all files, # ignoring the paths passed to this script, diff --git a/scripts/stop_test_env.sh b/scripts/stop_test_env.sh deleted file mode 100755 index 76ab4a3ee0..0000000000 --- a/scripts/stop_test_env.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -source ./scripts/set_variables.sh - -docker compose -p $DOCKER_COMPOSE_PROJECT -f docs/includes/docker-compose.yaml down diff --git a/scripts/test-cov.sh b/scripts/test-cov.sh deleted file mode 100755 index f8a4389eab..0000000000 --- a/scripts/test-cov.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -bash scripts/test.sh -m "all" "$@" - -coverage combine -coverage report --show-missing --skip-covered --sort=cover --precision=2 - -rm .coverage* diff --git a/scripts/test.sh b/scripts/test.sh deleted file mode 100755 index 09960abead..0000000000 --- a/scripts/test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -coverage run -m pytest -x --ff "$@" || \ -coverage run -m pytest -x --ff "$@" || \ -coverage run -m pytest -x --ff "$@" diff --git a/tests/a_docs/confluent/ack/test_errors.py b/tests/a_docs/confluent/ack/test_errors.py deleted file mode 100644 index 08017ba472..0000000000 --- a/tests/a_docs/confluent/ack/test_errors.py +++ /dev/null @@ -1,22 +0,0 @@ -from unittest.mock import patch - -import pytest - -from faststream.confluent import TestApp, TestKafkaBroker -from faststream.confluent.client import AsyncConfluentConsumer -from tests.tools import spy_decorator - - -@pytest.mark.asyncio -@pytest.mark.confluent -@pytest.mark.slow -async def test_ack_exc(): - from docs.docs_src.confluent.ack.errors import app, broker, handle - - with patch.object( - AsyncConfluentConsumer, "commit", spy_decorator(AsyncConfluentConsumer.commit) - ) as m: - async with TestKafkaBroker(broker, with_real=True), TestApp(app): - await handle.wait_call(20) - - assert m.mock.call_count diff --git a/tests/a_docs/confluent/additional_config/test_app.py b/tests/a_docs/confluent/additional_config/test_app.py deleted file mode 100644 index b93aec1667..0000000000 --- a/tests/a_docs/confluent/additional_config/test_app.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from docs.docs_src.confluent.additional_config.app import ( - HelloWorld, - broker, - on_hello_world, -) -from faststream.confluent import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_base_app(): - async with TestKafkaBroker(broker): - await broker.publish(HelloWorld(msg="First Hello"), "hello_world") - on_hello_world.mock.assert_called_with(dict(HelloWorld(msg="First Hello"))) diff --git a/tests/a_docs/confluent/basic/test_basic.py b/tests/a_docs/confluent/basic/test_basic.py deleted file mode 100644 index 60828d8564..0000000000 --- a/tests/a_docs/confluent/basic/test_basic.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from faststream.confluent import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_basic(): - from docs.docs_src.confluent.basic.basic import broker, on_input_data - - publisher = list(broker._publishers.values())[0] # noqa: RUF015 - - async with TestKafkaBroker(broker) as br: - await br.publish({"data": 1.0}, "input_data") - on_input_data.mock.assert_called_once_with({"data": 1.0}) - publisher.mock.assert_called_once_with({"data": 2.0}) diff --git a/tests/a_docs/confluent/basic/test_cmd_run.py b/tests/a_docs/confluent/basic/test_cmd_run.py deleted file mode 100644 index 46a63b4257..0000000000 --- a/tests/a_docs/confluent/basic/test_cmd_run.py +++ /dev/null @@ -1,40 +0,0 @@ -import asyncio -from unittest.mock import Mock - -import pytest -from typer.testing import CliRunner - -from faststream.app import FastStream -from faststream.cli.main import cli - - -@pytest.fixture -def confluent_basic_project(): - return "docs.docs_src.confluent.basic.basic:app" - - -@pytest.mark.confluent -def test_run_cmd( - runner: CliRunner, - mock: Mock, - event: asyncio.Event, - monkeypatch: pytest.MonkeyPatch, - confluent_basic_project, -): - async def patched_run(self: FastStream, *args, **kwargs): - await self.start() - await self.stop() - mock() - - with monkeypatch.context() as m: - m.setattr(FastStream, "run", patched_run) - r = runner.invoke( - cli, - [ - "run", - confluent_basic_project, - ], - ) - - assert r.exit_code == 0 - mock.assert_called_once() diff --git a/tests/a_docs/confluent/batch_consuming_pydantic/test_app.py b/tests/a_docs/confluent/batch_consuming_pydantic/test_app.py deleted file mode 100644 index 66539c1f06..0000000000 --- a/tests/a_docs/confluent/batch_consuming_pydantic/test_app.py +++ /dev/null @@ -1,21 +0,0 @@ -import pytest - -from docs.docs_src.confluent.batch_consuming_pydantic.app import ( - HelloWorld, - broker, - handle_batch, -) -from faststream.confluent import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_me(): - async with TestKafkaBroker(broker): - await broker.publish_batch( - HelloWorld(msg="First Hello"), - HelloWorld(msg="Second Hello"), - topic="test_batch", - ) - handle_batch.mock.assert_called_with( - [dict(HelloWorld(msg="First Hello")), dict(HelloWorld(msg="Second Hello"))] - ) diff --git a/tests/a_docs/confluent/consumes_basics/test_app.py b/tests/a_docs/confluent/consumes_basics/test_app.py deleted file mode 100644 index ae1016cafd..0000000000 --- a/tests/a_docs/confluent/consumes_basics/test_app.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from docs.docs_src.confluent.consumes_basics.app import ( - HelloWorld, - broker, - on_hello_world, -) -from faststream.confluent import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_base_app(): - async with TestKafkaBroker(broker): - await broker.publish(HelloWorld(msg="First Hello"), "hello_world") - on_hello_world.mock.assert_called_with(dict(HelloWorld(msg="First Hello"))) diff --git a/tests/a_docs/confluent/publish_batch/test_app.py b/tests/a_docs/confluent/publish_batch/test_app.py deleted file mode 100644 index 9e3b3ecbf3..0000000000 --- a/tests/a_docs/confluent/publish_batch/test_app.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest - -from docs.docs_src.confluent.publish_batch.app import ( - Data, - broker, - decrease_and_increase, - on_input_data_1, - on_input_data_2, -) -from faststream.confluent import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_batch_publish_decorator(): - async with TestKafkaBroker(broker): - await broker.publish(Data(data=2.0), "input_data_1") - - on_input_data_1.mock.assert_called_once_with(dict(Data(data=2.0))) - decrease_and_increase.mock.assert_called_once_with( - [dict(Data(data=1.0)), dict(Data(data=4.0))] - ) - - -@pytest.mark.asyncio -async def test_batch_publish_call(): - async with TestKafkaBroker(broker): - await broker.publish(Data(data=2.0), "input_data_2") - - on_input_data_2.mock.assert_called_once_with(dict(Data(data=2.0))) - decrease_and_increase.mock.assert_called_once_with( - [dict(Data(data=1.0)), dict(Data(data=4.0))] - ) diff --git a/tests/a_docs/confluent/publish_batch/test_issues.py b/tests/a_docs/confluent/publish_batch/test_issues.py deleted file mode 100644 index 1cbace9a89..0000000000 --- a/tests/a_docs/confluent/publish_batch/test_issues.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import List - -import pytest - -from faststream import FastStream -from faststream.confluent import KafkaBroker, TestKafkaBroker - -broker = KafkaBroker() -batch_producer = broker.publisher("response", batch=True) - - -@batch_producer -@broker.subscriber("test") -async def handle(msg: str) -> List[int]: - return [1, 2, 3] - - -app = FastStream(broker) - - -@pytest.mark.asyncio -async def test_base_app(): - async with TestKafkaBroker(broker): - await broker.publish("", "test") diff --git a/tests/a_docs/confluent/publish_example/test_app.py b/tests/a_docs/confluent/publish_example/test_app.py deleted file mode 100644 index 84d0d61bd8..0000000000 --- a/tests/a_docs/confluent/publish_example/test_app.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -from docs.docs_src.confluent.publish_example.app import ( - Data, - broker, - on_input_data, - to_output_data, -) -from faststream.confluent import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_base_app(): - async with TestKafkaBroker(broker): - await broker.publish(Data(data=0.2), "input_data") - - on_input_data.mock.assert_called_once_with(dict(Data(data=0.2))) - to_output_data.mock.assert_called_once_with(dict(Data(data=1.2))) diff --git a/tests/a_docs/confluent/publish_with_partition_key/test_app.py b/tests/a_docs/confluent/publish_with_partition_key/test_app.py deleted file mode 100644 index 8d2b98324a..0000000000 --- a/tests/a_docs/confluent/publish_with_partition_key/test_app.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest - -from docs.docs_src.confluent.publish_with_partition_key.app import ( - Data, - broker, - on_input_data, - to_output_data, -) -from faststream.confluent import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_app(): - async with TestKafkaBroker(broker): - await broker.publish(Data(data=0.2), "input_data", key=b"my_key") - - on_input_data.mock.assert_called_once_with(dict(Data(data=0.2))) - to_output_data.mock.assert_called_once_with(dict(Data(data=1.2))) - - -@pytest.mark.skip("we are not checking the key") -@pytest.mark.asyncio -async def test_keys(): - async with TestKafkaBroker(broker): - # we should be able to publish a message with the key - await broker.publish(Data(data=0.2), "input_data", key=b"my_key") - - # we need to check the key as well - on_input_data.mock.assert_called_once_with(dict(Data(data=0.2)), key=b"my_key") - to_output_data.mock.assert_called_once_with(dict(Data(data=1.2)), key=b"key") diff --git a/tests/a_docs/confluent/test_security.py b/tests/a_docs/confluent/test_security.py deleted file mode 100644 index bba3b12deb..0000000000 --- a/tests/a_docs/confluent/test_security.py +++ /dev/null @@ -1,153 +0,0 @@ -import pytest - -from tests.brokers.confluent.test_security import patch_aio_consumer_and_producer - - -@pytest.mark.asyncio -@pytest.mark.confluent -async def test_base_security(): - from docs.docs_src.confluent.security.basic import broker as basic_broker - - with patch_aio_consumer_and_producer() as producer: - async with basic_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = {} - - assert call_kwargs.items() <= producer_call_kwargs.items() - - -@pytest.mark.asyncio -@pytest.mark.confluent -async def test_scram256(): - from docs.docs_src.confluent.security.sasl_scram256 import ( - broker as scram256_broker, - ) - - with patch_aio_consumer_and_producer() as producer: - async with scram256_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = { - "security_config": { - "sasl.mechanism": "SCRAM-SHA-256", - "sasl.username": "admin", - "sasl.password": "password", # pragma: allowlist secret - }, - "security_protocol": "SASL_SSL", - } - - assert call_kwargs.items() <= producer_call_kwargs.items() - - assert ( - producer_call_kwargs["security_protocol"] - == call_kwargs["security_protocol"] - ) - - -@pytest.mark.asyncio -@pytest.mark.confluent -async def test_scram512(): - from docs.docs_src.confluent.security.sasl_scram512 import ( - broker as scram512_broker, - ) - - with patch_aio_consumer_and_producer() as producer: - async with scram512_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = { - "security_config": { - "sasl.mechanism": "SCRAM-SHA-512", - "sasl.username": "admin", - "sasl.password": "password", # pragma: allowlist secret - }, - "security_protocol": "SASL_SSL", - } - - assert call_kwargs.items() <= producer_call_kwargs.items() - - assert ( - producer_call_kwargs["security_protocol"] - == call_kwargs["security_protocol"] - ) - - -@pytest.mark.asyncio -@pytest.mark.confluent -async def test_plaintext(): - from docs.docs_src.confluent.security.plaintext import ( - broker as plaintext_broker, - ) - - with patch_aio_consumer_and_producer() as producer: - async with plaintext_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = { - "security_config": { - "sasl.mechanism": "PLAIN", - "sasl.username": "admin", - "sasl.password": "password", # pragma: allowlist secret - }, - "security_protocol": "SASL_SSL", - } - - assert call_kwargs.items() <= producer_call_kwargs.items() - - assert ( - producer_call_kwargs["security_protocol"] - == call_kwargs["security_protocol"] - ) - - -@pytest.mark.asyncio -@pytest.mark.confluent -async def test_oathbearer(): - from docs.docs_src.confluent.security.sasl_oauthbearer import ( - broker as oauthbearer_broker, - ) - - with patch_aio_consumer_and_producer() as producer: - async with oauthbearer_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = { - "security_config": { - "sasl.mechanism": "OAUTHBEARER", - }, - "security_protocol": "SASL_SSL", - } - - assert call_kwargs.items() <= producer_call_kwargs.items() - - assert ( - producer_call_kwargs["security_protocol"] - == call_kwargs["security_protocol"] - ) - - -@pytest.mark.asyncio -@pytest.mark.confluent -async def test_gssapi(): - from docs.docs_src.confluent.security.sasl_gssapi import ( - broker as gssapi_broker, - ) - - with patch_aio_consumer_and_producer() as producer: - async with gssapi_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = { - "security_config": { - "sasl.mechanism": "GSSAPI", - }, - "security_protocol": "SASL_SSL", - } - - assert call_kwargs.items() <= producer_call_kwargs.items() - - assert ( - producer_call_kwargs["security_protocol"] - == call_kwargs["security_protocol"] - ) diff --git a/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_basic.py b/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_basic.py deleted file mode 100644 index a2167425a5..0000000000 --- a/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_basic.py +++ /dev/null @@ -1,64 +0,0 @@ -from docs.docs_src.getting_started.asyncapi.asyncapi_customization.basic import app -from faststream.asyncapi.generate import get_app_schema - - -def test_basic_customization(): - schema = get_app_schema(app).to_jsonable() - assert schema == { - "asyncapi": "2.6.0", - "channels": { - "input_data:OnInputData": { - "bindings": { - "kafka": {"bindingVersion": "0.4.0", "topic": "input_data"} - }, - "servers": ["development"], - "subscribe": { - "message": { - "$ref": "#/components/messages/input_data:OnInputData:Message" - } - }, - }, - "output_data:Publisher": { - "bindings": { - "kafka": {"bindingVersion": "0.4.0", "topic": "output_data"} - }, - "publish": { - "message": { - "$ref": "#/components/messages/output_data:Publisher:Message" - } - }, - "servers": ["development"], - }, - }, - "components": { - "messages": { - "input_data:OnInputData:Message": { - "correlationId": {"location": "$message.header#/correlation_id"}, - "payload": { - "$ref": "#/components/schemas/OnInputData:Message:Payload" - }, - "title": "input_data:OnInputData:Message", - }, - "output_data:Publisher:Message": { - "correlationId": {"location": "$message.header#/correlation_id"}, - "payload": { - "$ref": "#/components/schemas/output_data:PublisherPayload" - }, - "title": "output_data:Publisher:Message", - }, - }, - "schemas": { - "OnInputData:Message:Payload": {"title": "OnInputData:Message:Payload"}, - "output_data:PublisherPayload": {}, - }, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "localhost:9092", - } - }, - } diff --git a/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_broker.py b/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_broker.py deleted file mode 100644 index 25c886853d..0000000000 --- a/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_broker.py +++ /dev/null @@ -1,17 +0,0 @@ -from docs.docs_src.getting_started.asyncapi.asyncapi_customization.custom_broker import ( - app, -) -from faststream.asyncapi.generate import get_app_schema - - -def test_broker_customization(): - schema = get_app_schema(app).to_jsonable() - - assert schema["servers"] == { - "development": { - "url": "non-sensitive-url:9092", - "protocol": "kafka", - "description": "Kafka broker running locally", - "protocolVersion": "auto", - } - } diff --git a/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_handler.py b/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_handler.py deleted file mode 100644 index 31eead7a3b..0000000000 --- a/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_handler.py +++ /dev/null @@ -1,41 +0,0 @@ -from dirty_equals import IsPartialDict - -from docs.docs_src.getting_started.asyncapi.asyncapi_customization.custom_handler import ( - app, -) -from faststream.asyncapi.generate import get_app_schema - - -def test_handler_customization(): - schema = get_app_schema(app).to_jsonable() - - (subscriber_key, subscriber_value), (publisher_key, publisher_value) = schema[ - "channels" - ].items() - - assert subscriber_key == "input_data:Consume", subscriber_key - assert subscriber_value == IsPartialDict( - { - "servers": ["development"], - "bindings": {"kafka": {"topic": "input_data", "bindingVersion": "0.4.0"}}, - "subscribe": { - "message": {"$ref": "#/components/messages/input_data:Consume:Message"} - }, - } - ), subscriber_value - desc = subscriber_value["description"] - assert ( # noqa: PT018 - "Consumer function\n\n" in desc - and "Args:\n" in desc - and " msg: input msg" in desc - ), desc - - assert publisher_key == "output_data:Produce", publisher_key - assert publisher_value == { - "description": "My publisher description", - "servers": ["development"], - "bindings": {"kafka": {"topic": "output_data", "bindingVersion": "0.4.0"}}, - "publish": { - "message": {"$ref": "#/components/messages/output_data:Produce:Message"} - }, - } diff --git a/tests/a_docs/getting_started/cli/confluent/test_confluent_context.py b/tests/a_docs/getting_started/cli/confluent/test_confluent_context.py deleted file mode 100644 index fa686ad864..0000000000 --- a/tests/a_docs/getting_started/cli/confluent/test_confluent_context.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from faststream import TestApp, context -from faststream.confluent import TestKafkaBroker -from tests.marks import pydantic_v2 -from tests.mocks import mock_pydantic_settings_env - - -@pydantic_v2 -@pytest.mark.asyncio -async def test(): - with mock_pydantic_settings_env({"host": "localhost"}): - from docs.docs_src.getting_started.cli.confluent_context import app, broker - - async with TestKafkaBroker(broker), TestApp(app, {"env": ""}): - assert context.get("settings").host == "localhost" diff --git a/tests/a_docs/getting_started/cli/kafka/test_kafka_context.py b/tests/a_docs/getting_started/cli/kafka/test_kafka_context.py deleted file mode 100644 index 8f1cb886c9..0000000000 --- a/tests/a_docs/getting_started/cli/kafka/test_kafka_context.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from faststream import TestApp, context -from faststream.kafka import TestKafkaBroker -from tests.marks import pydantic_v2 -from tests.mocks import mock_pydantic_settings_env - - -@pydantic_v2 -@pytest.mark.asyncio -async def test(): - with mock_pydantic_settings_env({"host": "localhost"}): - from docs.docs_src.getting_started.cli.kafka_context import app, broker - - async with TestKafkaBroker(broker), TestApp(app, {"env": ""}): - assert context.get("settings").host == "localhost" diff --git a/tests/a_docs/getting_started/cli/nats/test_nats_context.py b/tests/a_docs/getting_started/cli/nats/test_nats_context.py deleted file mode 100644 index 3f6764861b..0000000000 --- a/tests/a_docs/getting_started/cli/nats/test_nats_context.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from faststream import TestApp, context -from faststream.nats import TestNatsBroker -from tests.marks import pydantic_v2 -from tests.mocks import mock_pydantic_settings_env - - -@pydantic_v2 -@pytest.mark.asyncio -async def test(): - with mock_pydantic_settings_env({"host": "localhost"}): - from docs.docs_src.getting_started.cli.nats_context import app, broker - - async with TestNatsBroker(broker), TestApp(app, {"env": ""}): - assert context.get("settings").host == "localhost" diff --git a/tests/a_docs/getting_started/cli/rabbit/test_rabbit_context.py b/tests/a_docs/getting_started/cli/rabbit/test_rabbit_context.py deleted file mode 100644 index 71d55dcb34..0000000000 --- a/tests/a_docs/getting_started/cli/rabbit/test_rabbit_context.py +++ /dev/null @@ -1,21 +0,0 @@ -import pytest - -from faststream import TestApp, context -from faststream.rabbit import TestRabbitBroker -from tests.marks import pydantic_v2 -from tests.mocks import mock_pydantic_settings_env - - -@pydantic_v2 -@pytest.mark.asyncio -async def test(): - with mock_pydantic_settings_env( - {"host": "amqp://guest:guest@localhost:5673/"} # pragma: allowlist secret - ): - from docs.docs_src.getting_started.cli.rabbit_context import app, broker - - async with TestRabbitBroker(broker), TestApp(app, {"env": ".env"}): - assert ( - context.get("settings").host - == "amqp://guest:guest@localhost:5673/" # pragma: allowlist secret - ) diff --git a/tests/a_docs/getting_started/cli/redis/test_redis_context.py b/tests/a_docs/getting_started/cli/redis/test_redis_context.py deleted file mode 100644 index 1696bbcf61..0000000000 --- a/tests/a_docs/getting_started/cli/redis/test_redis_context.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from faststream import TestApp, context -from faststream.redis import TestRedisBroker -from tests.marks import pydantic_v2 -from tests.mocks import mock_pydantic_settings_env - - -@pydantic_v2 -@pytest.mark.asyncio -async def test(): - with mock_pydantic_settings_env({"host": "redis://localhost:6380"}): - from docs.docs_src.getting_started.cli.redis_context import app, broker - - async with TestRedisBroker(broker), TestApp(app, {"env": ".env"}): - assert context.get("settings").host == "redis://localhost:6380" diff --git a/tests/a_docs/getting_started/context/test_annotated.py b/tests/a_docs/getting_started/context/test_annotated.py deleted file mode 100644 index 4e07a231d1..0000000000 --- a/tests/a_docs/getting_started/context/test_annotated.py +++ /dev/null @@ -1,90 +0,0 @@ -import pytest - -from tests.marks import ( - python39, - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@python39 -@pytest.mark.asyncio -@require_aiokafka -async def test_annotated_kafka(): - from docs.docs_src.getting_started.context.kafka.annotated import ( - base_handler, - broker, - ) - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker) as br: - await br.publish("Hi!", "test") - - base_handler.mock.assert_called_once_with("Hi!") - - -@python39 -@pytest.mark.asyncio -@require_confluent -async def test_annotated_confluent(): - from docs.docs_src.getting_started.context.confluent.annotated import ( - base_handler, - broker, - ) - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker) as br: - await br.publish("Hi!", "test") - - base_handler.mock.assert_called_once_with("Hi!") - - -@python39 -@pytest.mark.asyncio -@require_aiopika -async def test_annotated_rabbit(): - from docs.docs_src.getting_started.context.rabbit.annotated import ( - base_handler, - broker, - ) - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker) as br: - await br.publish("Hi!", "test") - - base_handler.mock.assert_called_once_with("Hi!") - - -@python39 -@pytest.mark.asyncio -@require_nats -async def test_annotated_nats(): - from docs.docs_src.getting_started.context.nats.annotated import ( - base_handler, - broker, - ) - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker) as br: - await br.publish("Hi!", "test") - - base_handler.mock.assert_called_once_with("Hi!") - - -@python39 -@pytest.mark.asyncio -@require_redis -async def test_annotated_redis(): - from docs.docs_src.getting_started.context.redis.annotated import ( - base_handler, - broker, - ) - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker) as br: - await br.publish("Hi!", "test") - - base_handler.mock.assert_called_once_with("Hi!") diff --git a/tests/a_docs/getting_started/context/test_base.py b/tests/a_docs/getting_started/context/test_base.py deleted file mode 100644 index d2fa65ebe2..0000000000 --- a/tests/a_docs/getting_started/context/test_base.py +++ /dev/null @@ -1,72 +0,0 @@ -import pytest - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_base_kafka(): - from docs.docs_src.getting_started.context.kafka.base import base_handler, broker - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker) as br: - await br.publish("Hi!", "test") - - base_handler.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_confluent -async def test_base_confluent(): - from docs.docs_src.getting_started.context.confluent.base import ( - base_handler, - broker, - ) - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker) as br: - await br.publish("Hi!", "test") - - base_handler.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_aiopika -async def test_base_rabbit(): - from docs.docs_src.getting_started.context.rabbit.base import base_handler, broker - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker) as br: - await br.publish("Hi!", "test") - - base_handler.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_nats -async def test_base_nats(): - from docs.docs_src.getting_started.context.nats.base import base_handler, broker - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker) as br: - await br.publish("Hi!", "test") - - base_handler.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_redis -async def test_base_redis(): - from docs.docs_src.getting_started.context.redis.base import base_handler, broker - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker) as br: - await br.publish("Hi!", "test") - - base_handler.mock.assert_called_once_with("Hi!") diff --git a/tests/a_docs/getting_started/context/test_initial.py b/tests/a_docs/getting_started/context/test_initial.py deleted file mode 100644 index 799bfd173a..0000000000 --- a/tests/a_docs/getting_started/context/test_initial.py +++ /dev/null @@ -1,86 +0,0 @@ -import pytest - -from faststream import context -from tests.marks import ( - python39, - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@python39 -@require_aiokafka -async def test_kafka(): - from docs.docs_src.getting_started.context.kafka.initial import broker - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker) as br: - await br.publish("", "test-topic") - await br.publish("", "test-topic") - - assert context.get("collector") == ["", ""] - context.clear() - - -@pytest.mark.asyncio -@python39 -@require_confluent -async def test_confluent(): - from docs.docs_src.getting_started.context.confluent.initial import broker - from faststream.confluent import TestKafkaBroker - - async with TestKafkaBroker(broker) as br: - await br.publish("", "test-topic") - await br.publish("", "test-topic") - - assert context.get("collector") == ["", ""] - context.clear() - - -@pytest.mark.asyncio -@python39 -@require_aiopika -async def test_rabbit(): - from docs.docs_src.getting_started.context.rabbit.initial import broker - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker) as br: - await br.publish("", "test-queue") - await br.publish("", "test-queue") - - assert context.get("collector") == ["", ""] - context.clear() - - -@pytest.mark.asyncio -@python39 -@require_nats -async def test_nats(): - from docs.docs_src.getting_started.context.nats.initial import broker - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker) as br: - await br.publish("", "test-subject") - await br.publish("", "test-subject") - - assert context.get("collector") == ["", ""] - context.clear() - - -@pytest.mark.asyncio -@python39 -@require_redis -async def test_redis(): - from docs.docs_src.getting_started.context.redis.initial import broker - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker) as br: - await br.publish("", "test-channel") - await br.publish("", "test-channel") - - assert context.get("collector") == ["", ""] - context.clear() diff --git a/tests/a_docs/getting_started/dependencies/basic/test_depends.py b/tests/a_docs/getting_started/dependencies/basic/test_depends.py deleted file mode 100644 index 5c9027d5eb..0000000000 --- a/tests/a_docs/getting_started/dependencies/basic/test_depends.py +++ /dev/null @@ -1,79 +0,0 @@ -import pytest - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_depends_kafka(): - from docs.docs_src.getting_started.dependencies.basic.kafka.depends import ( - broker, - handler, - ) - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker): - await broker.publish({}, "test") - handler.mock.assert_called_once_with({}) - - -@pytest.mark.asyncio -@require_confluent -async def test_depends_confluent(): - from docs.docs_src.getting_started.dependencies.basic.confluent.depends import ( - broker, - handler, - ) - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker): - await broker.publish({}, "test") - handler.mock.assert_called_once_with({}) - - -@pytest.mark.asyncio -@require_aiopika -async def test_depends_rabbit(): - from docs.docs_src.getting_started.dependencies.basic.rabbit.depends import ( - broker, - handler, - ) - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker): - await broker.publish({}, "test") - handler.mock.assert_called_once_with({}) - - -@pytest.mark.asyncio -@require_nats -async def test_depends_nats(): - from docs.docs_src.getting_started.dependencies.basic.nats.depends import ( - broker, - handler, - ) - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker): - await broker.publish({}, "test") - handler.mock.assert_called_once_with({}) - - -@pytest.mark.asyncio -@require_redis -async def test_depends_redis(): - from docs.docs_src.getting_started.dependencies.basic.redis.depends import ( - broker, - handler, - ) - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker): - await broker.publish({}, "test") - handler.mock.assert_called_once_with({}) diff --git a/tests/a_docs/getting_started/dependencies/test_basic.py b/tests/a_docs/getting_started/dependencies/test_basic.py deleted file mode 100644 index 79add9edc5..0000000000 --- a/tests/a_docs/getting_started/dependencies/test_basic.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - -from faststream import TestApp -from tests.marks import require_aiokafka - - -@pytest.mark.asyncio -@require_aiokafka -async def test_basic_kafka(): - from docs.docs_src.getting_started.dependencies.basic_kafka import ( - app, - broker, - handle, - ) - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker), TestApp(app): - handle.mock.assert_called_once_with( - { - "name": "John", - "user_id": 1, - } - ) diff --git a/tests/a_docs/getting_started/index/test_basic.py b/tests/a_docs/getting_started/index/test_basic.py deleted file mode 100644 index ed05ff61d4..0000000000 --- a/tests/a_docs/getting_started/index/test_basic.py +++ /dev/null @@ -1,69 +0,0 @@ -import pytest - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_quickstart_index_kafka(): - from docs.docs_src.getting_started.index.base_kafka import base_handler, broker - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker) as br: - await br.publish("", "test") - - base_handler.mock.assert_called_once_with("") - - -@pytest.mark.asyncio -@require_confluent -async def test_quickstart_index_confluent(): - from docs.docs_src.getting_started.index.base_confluent import base_handler, broker - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker) as br: - await br.publish("", "test") - - base_handler.mock.assert_called_once_with("") - - -@pytest.mark.asyncio -@require_aiopika -async def test_quickstart_index_rabbit(): - from docs.docs_src.getting_started.index.base_rabbit import base_handler, broker - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker) as br: - await br.publish("", "test") - - base_handler.mock.assert_called_once_with("") - - -@pytest.mark.asyncio -@require_nats -async def test_quickstart_index_nats(): - from docs.docs_src.getting_started.index.base_nats import base_handler, broker - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker) as br: - await br.publish("", "test") - - base_handler.mock.assert_called_once_with("") - - -@pytest.mark.asyncio -@require_redis -async def test_quickstart_index_redis(): - from docs.docs_src.getting_started.index.base_redis import base_handler, broker - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker) as br: - await br.publish("", "test") - - base_handler.mock.assert_called_once_with("") diff --git a/tests/a_docs/getting_started/lifespan/test_basic.py b/tests/a_docs/getting_started/lifespan/test_basic.py deleted file mode 100644 index e301441660..0000000000 --- a/tests/a_docs/getting_started/lifespan/test_basic.py +++ /dev/null @@ -1,77 +0,0 @@ -import pytest - -from faststream import TestApp, context -from tests.marks import ( - pydantic_v2, - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) -from tests.mocks import mock_pydantic_settings_env - - -@pydantic_v2 -@pytest.mark.asyncio -@require_aiopika -async def test_rabbit_basic_lifespan(): - from faststream.rabbit import TestRabbitBroker - - with mock_pydantic_settings_env({"host": "localhost"}): - from docs.docs_src.getting_started.lifespan.rabbit.basic import app, broker - - async with TestRabbitBroker(broker), TestApp(app): - assert context.get("settings").host == "localhost" - - -@pydantic_v2 -@pytest.mark.asyncio -@require_aiokafka -async def test_kafka_basic_lifespan(): - from faststream.kafka import TestKafkaBroker - - with mock_pydantic_settings_env({"host": "localhost"}): - from docs.docs_src.getting_started.lifespan.kafka.basic import app, broker - - async with TestKafkaBroker(broker), TestApp(app): - assert context.get("settings").host == "localhost" - - -@pydantic_v2 -@pytest.mark.asyncio -@require_confluent -async def test_confluent_basic_lifespan(): - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - with mock_pydantic_settings_env({"host": "localhost"}): - from docs.docs_src.getting_started.lifespan.confluent.basic import app, broker - - async with TestConfluentKafkaBroker(broker), TestApp(app): - assert context.get("settings").host == "localhost" - - -@pydantic_v2 -@pytest.mark.asyncio -@require_nats -async def test_nats_basic_lifespan(): - from faststream.nats import TestNatsBroker - - with mock_pydantic_settings_env({"host": "localhost"}): - from docs.docs_src.getting_started.lifespan.nats.basic import app, broker - - async with TestNatsBroker(broker), TestApp(app): - assert context.get("settings").host == "localhost" - - -@pydantic_v2 -@pytest.mark.asyncio -@require_redis -async def test_redis_basic_lifespan(): - from faststream.redis import TestRedisBroker - - with mock_pydantic_settings_env({"host": "localhost"}): - from docs.docs_src.getting_started.lifespan.redis.basic import app, broker - - async with TestRedisBroker(broker), TestApp(app): - assert context.get("settings").host == "localhost" diff --git a/tests/a_docs/getting_started/lifespan/test_ml.py b/tests/a_docs/getting_started/lifespan/test_ml.py deleted file mode 100644 index 0060f0719a..0000000000 --- a/tests/a_docs/getting_started/lifespan/test_ml.py +++ /dev/null @@ -1,70 +0,0 @@ -import pytest - -from faststream import TestApp -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiopika -async def test_rabbit_ml_lifespan(): - from docs.docs_src.getting_started.lifespan.rabbit.ml import app, broker, predict - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker), TestApp(app): - assert await broker.publish(1.0, "test", rpc=True) == {"result": 42.0} - - predict.mock.assert_called_once_with(1.0) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_kafka_ml_lifespan(): - from docs.docs_src.getting_started.lifespan.kafka.ml import app, broker, predict - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker), TestApp(app): - assert await broker.publish(1.0, "test", rpc=True) == {"result": 42.0} - - predict.mock.assert_called_once_with(1.0) - - -@pytest.mark.asyncio -@require_confluent -async def test_confluent_ml_lifespan(): - from docs.docs_src.getting_started.lifespan.confluent.ml import app, broker, predict - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker), TestApp(app): - assert await broker.publish(1.0, "test", rpc=True) == {"result": 42.0} - - predict.mock.assert_called_once_with(1.0) - - -@pytest.mark.asyncio -@require_nats -async def test_nats_ml_lifespan(): - from docs.docs_src.getting_started.lifespan.nats.ml import app, broker, predict - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker), TestApp(app): - assert await broker.publish(1.0, "test", rpc=True) == {"result": 42.0} - - predict.mock.assert_called_once_with(1.0) - - -@pytest.mark.asyncio -@require_redis -async def test_redis_ml_lifespan(): - from docs.docs_src.getting_started.lifespan.redis.ml import app, broker, predict - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker), TestApp(app): - assert await broker.publish(1.0, "test", rpc=True) == {"result": 42.0} - - predict.mock.assert_called_once_with(1.0) diff --git a/tests/a_docs/getting_started/lifespan/test_ml_context.py b/tests/a_docs/getting_started/lifespan/test_ml_context.py deleted file mode 100644 index e239e831a9..0000000000 --- a/tests/a_docs/getting_started/lifespan/test_ml_context.py +++ /dev/null @@ -1,90 +0,0 @@ -import pytest - -from faststream import TestApp -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiopika -async def test_rabbit_ml_lifespan(): - from docs.docs_src.getting_started.lifespan.rabbit.ml_context import ( - app, - broker, - predict, - ) - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker), TestApp(app): - assert await broker.publish(1.0, "test", rpc=True) == {"result": 42.0} - - predict.mock.assert_called_once_with(1.0) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_kafka_ml_lifespan(): - from docs.docs_src.getting_started.lifespan.kafka.ml_context import ( - app, - broker, - predict, - ) - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker), TestApp(app): - assert await broker.publish(1.0, "test", rpc=True) == {"result": 42.0} - - predict.mock.assert_called_once_with(1.0) - - -@pytest.mark.asyncio -@require_confluent -async def test_confluent_ml_lifespan(): - from docs.docs_src.getting_started.lifespan.confluent.ml_context import ( - app, - broker, - predict, - ) - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker), TestApp(app): - assert await broker.publish(1.0, "test", rpc=True) == {"result": 42.0} - - predict.mock.assert_called_once_with(1.0) - - -@pytest.mark.asyncio -@require_nats -async def test_nats_ml_lifespan(): - from docs.docs_src.getting_started.lifespan.nats.ml_context import ( - app, - broker, - predict, - ) - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker), TestApp(app): - assert await broker.publish(1.0, "test", rpc=True) == {"result": 42.0} - - predict.mock.assert_called_once_with(1.0) - - -@pytest.mark.asyncio -@require_redis -async def test_redis_ml_lifespan(): - from docs.docs_src.getting_started.lifespan.redis.ml_context import ( - app, - broker, - predict, - ) - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker), TestApp(app): - assert await broker.publish(1.0, "test", rpc=True) == {"result": 42.0} - - predict.mock.assert_called_once_with(1.0) diff --git a/tests/a_docs/getting_started/lifespan/test_multi.py b/tests/a_docs/getting_started/lifespan/test_multi.py deleted file mode 100644 index eb272f3e2a..0000000000 --- a/tests/a_docs/getting_started/lifespan/test_multi.py +++ /dev/null @@ -1,11 +0,0 @@ -import pytest - -from faststream import TestApp, context - - -@pytest.mark.asyncio -async def test_multi_lifespan(): - from docs.docs_src.getting_started.lifespan.multiple import app - - async with TestApp(app): - assert context.get("field") == 1 diff --git a/tests/a_docs/getting_started/lifespan/test_testing.py b/tests/a_docs/getting_started/lifespan/test_testing.py deleted file mode 100644 index 796675e1f0..0000000000 --- a/tests/a_docs/getting_started/lifespan/test_testing.py +++ /dev/null @@ -1,65 +0,0 @@ -import pytest - -from tests.marks import ( - python39, - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@python39 -@require_redis -async def test_lifespan_redis(): - from docs.docs_src.getting_started.lifespan.redis.testing import ( - test_lifespan as _test_lifespan_red, - ) - - await _test_lifespan_red() - - -@pytest.mark.asyncio -@python39 -@require_confluent -async def test_lifespan_confluent(): - from docs.docs_src.getting_started.lifespan.confluent.testing import ( - test_lifespan as _test_lifespan_confluent, - ) - - await _test_lifespan_confluent() - - -@pytest.mark.asyncio -@python39 -@require_aiokafka -async def test_lifespan_kafka(): - from docs.docs_src.getting_started.lifespan.kafka.testing import ( - test_lifespan as _test_lifespan_k, - ) - - await _test_lifespan_k() - - -@pytest.mark.asyncio -@python39 -@require_aiopika -async def test_lifespan_rabbit(): - from docs.docs_src.getting_started.lifespan.rabbit.testing import ( - test_lifespan as _test_lifespan_r, - ) - - await _test_lifespan_r() - - -@pytest.mark.asyncio -@python39 -@require_nats -async def test_lifespan_nats(): - from docs.docs_src.getting_started.lifespan.nats.testing import ( - test_lifespan as _test_lifespan_n, - ) - - await _test_lifespan_n() diff --git a/tests/a_docs/getting_started/publishing/test_broker.py b/tests/a_docs/getting_started/publishing/test_broker.py deleted file mode 100644 index 94947dddd1..0000000000 --- a/tests/a_docs/getting_started/publishing/test_broker.py +++ /dev/null @@ -1,90 +0,0 @@ -import pytest - -from faststream import TestApp -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_broker_kafka(): - from docs.docs_src.getting_started.publishing.kafka.broker import ( - app, - broker, - handle, - handle_next, - ) - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker), TestApp(app): - handle.mock.assert_called_once_with("") - handle_next.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_confluent -async def test_broker_confluent(): - from docs.docs_src.getting_started.publishing.confluent.broker import ( - app, - broker, - handle, - handle_next, - ) - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker), TestApp(app): - handle.mock.assert_called_once_with("") - handle_next.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_aiopika -async def test_broker_rabbit(): - from docs.docs_src.getting_started.publishing.rabbit.broker import ( - app, - broker, - handle, - handle_next, - ) - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker), TestApp(app): - handle.mock.assert_called_once_with("") - handle_next.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_nats -async def test_broker_nats(): - from docs.docs_src.getting_started.publishing.nats.broker import ( - app, - broker, - handle, - handle_next, - ) - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker), TestApp(app): - handle.mock.assert_called_once_with("") - handle_next.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_redis -async def test_broker_redis(): - from docs.docs_src.getting_started.publishing.redis.broker import ( - app, - broker, - handle, - handle_next, - ) - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker), TestApp(app): - handle.mock.assert_called_once_with("") - handle_next.mock.assert_called_once_with("Hi!") diff --git a/tests/a_docs/getting_started/publishing/test_decorator.py b/tests/a_docs/getting_started/publishing/test_decorator.py deleted file mode 100644 index e97d65e567..0000000000 --- a/tests/a_docs/getting_started/publishing/test_decorator.py +++ /dev/null @@ -1,95 +0,0 @@ -import pytest - -from faststream import TestApp -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_decorator_kafka(): - from docs.docs_src.getting_started.publishing.kafka.decorator import ( - app, - broker, - handle, - handle_next, - ) - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker), TestApp(app): - handle.mock.assert_called_once_with("") - handle_next.mock.assert_called_once_with("Hi!") - next(iter(broker._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_confluent -async def test_decorator_confluent(): - from docs.docs_src.getting_started.publishing.confluent.decorator import ( - app, - broker, - handle, - handle_next, - ) - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker), TestApp(app): - handle.mock.assert_called_once_with("") - handle_next.mock.assert_called_once_with("Hi!") - next(iter(broker._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_aiopika -async def test_decorator_rabbit(): - from docs.docs_src.getting_started.publishing.rabbit.decorator import ( - app, - broker, - handle, - handle_next, - ) - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker), TestApp(app): - handle.mock.assert_called_once_with("") - handle_next.mock.assert_called_once_with("Hi!") - next(iter(broker._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_nats -async def test_decorator_nats(): - from docs.docs_src.getting_started.publishing.nats.decorator import ( - app, - broker, - handle, - handle_next, - ) - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker), TestApp(app): - handle.mock.assert_called_once_with("") - handle_next.mock.assert_called_once_with("Hi!") - next(iter(broker._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_redis -async def test_decorator_redis(): - from docs.docs_src.getting_started.publishing.redis.decorator import ( - app, - broker, - handle, - handle_next, - ) - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker), TestApp(app): - handle.mock.assert_called_once_with("") - handle_next.mock.assert_called_once_with("Hi!") - next(iter(broker._publishers.values())).mock.assert_called_once_with("Hi!") diff --git a/tests/a_docs/getting_started/publishing/test_direct.py b/tests/a_docs/getting_started/publishing/test_direct.py deleted file mode 100644 index d7be3a59a0..0000000000 --- a/tests/a_docs/getting_started/publishing/test_direct.py +++ /dev/null @@ -1,59 +0,0 @@ -import pytest - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_handle_kafka(): - from docs.docs_src.getting_started.publishing.kafka.direct_testing import ( - test_handle as test_handle_k, - ) - - await test_handle_k() - - -@pytest.mark.asyncio -@require_confluent -async def test_handle_confluent(): - from docs.docs_src.getting_started.publishing.confluent.direct_testing import ( - test_handle as test_handle_confluent, - ) - - await test_handle_confluent() - - -@pytest.mark.asyncio -@require_aiopika -async def test_handle_rabbit(): - from docs.docs_src.getting_started.publishing.rabbit.direct_testing import ( - test_handle as test_handle_r, - ) - - await test_handle_r() - - -@pytest.mark.asyncio -@require_nats -async def test_handle_nats(): - from docs.docs_src.getting_started.publishing.nats.direct_testing import ( - test_handle as test_handle_n, - ) - - await test_handle_n() - - -@pytest.mark.asyncio -@require_redis -async def test_handle_redis(): - from docs.docs_src.getting_started.publishing.redis.direct_testing import ( - test_handle as test_handle_red, - ) - - await test_handle_red() diff --git a/tests/a_docs/getting_started/publishing/test_object.py b/tests/a_docs/getting_started/publishing/test_object.py deleted file mode 100644 index 48f3e4b0a9..0000000000 --- a/tests/a_docs/getting_started/publishing/test_object.py +++ /dev/null @@ -1,59 +0,0 @@ -import pytest - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_handle_kafka(): - from docs.docs_src.getting_started.publishing.kafka.object_testing import ( - test_handle as test_handle_k, - ) - - await test_handle_k() - - -@pytest.mark.asyncio -@require_confluent -async def test_handle_confluent(): - from docs.docs_src.getting_started.publishing.confluent.object_testing import ( - test_handle as test_handle_confluent, - ) - - await test_handle_confluent() - - -@pytest.mark.asyncio -@require_aiopika -async def test_handle_rabbit(): - from docs.docs_src.getting_started.publishing.rabbit.object_testing import ( - test_handle as test_handle_r, - ) - - await test_handle_r() - - -@pytest.mark.asyncio -@require_nats -async def test_handle_nats(): - from docs.docs_src.getting_started.publishing.nats.object_testing import ( - test_handle as test_handle_n, - ) - - await test_handle_n() - - -@pytest.mark.asyncio -@require_redis -async def test_handle_redis(): - from docs.docs_src.getting_started.publishing.redis.object_testing import ( - test_handle as test_handle_red, - ) - - await test_handle_red() diff --git a/tests/a_docs/getting_started/routers/test_base.py b/tests/a_docs/getting_started/routers/test_base.py deleted file mode 100644 index 1d77657cf2..0000000000 --- a/tests/a_docs/getting_started/routers/test_base.py +++ /dev/null @@ -1,90 +0,0 @@ -import pytest - -from faststream import TestApp -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_base_router_kafka(): - from docs.docs_src.getting_started.routers.kafka.router import ( - app, - broker, - handle, - handle_response, - ) - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker), TestApp(app): - handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) - handle_response.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_confluent -async def test_base_router_confluent(): - from docs.docs_src.getting_started.routers.confluent.router import ( - app, - broker, - handle, - handle_response, - ) - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker), TestApp(app): - handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) - handle_response.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_aiopika -async def test_base_router_rabbit(): - from docs.docs_src.getting_started.routers.rabbit.router import ( - app, - broker, - handle, - handle_response, - ) - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker), TestApp(app): - handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) - handle_response.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_nats -async def test_base_router_nats(): - from docs.docs_src.getting_started.routers.nats.router import ( - app, - broker, - handle, - handle_response, - ) - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker), TestApp(app): - handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) - handle_response.mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_redis -async def test_base_router_redis(): - from docs.docs_src.getting_started.routers.redis.router import ( - app, - broker, - handle, - handle_response, - ) - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker), TestApp(app): - handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) - handle_response.mock.assert_called_once_with("Hi!") diff --git a/tests/a_docs/getting_started/routers/test_delay.py b/tests/a_docs/getting_started/routers/test_delay.py deleted file mode 100644 index 733c278fe5..0000000000 --- a/tests/a_docs/getting_started/routers/test_delay.py +++ /dev/null @@ -1,95 +0,0 @@ -import pytest - -from faststream import TestApp -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_delay_router_kafka(): - from docs.docs_src.getting_started.routers.kafka.router_delay import ( - app, - broker, - ) - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker) as br, TestApp(app): - next(iter(br._subscribers.values())).calls[ - 0 - ].handler.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - next(iter(br._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_confluent -async def test_delay_router_confluent(): - from docs.docs_src.getting_started.routers.confluent.router_delay import ( - app, - broker, - ) - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker) as br, TestApp(app): - next(iter(br._subscribers.values())).calls[ - 0 - ].handler.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - next(iter(br._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_aiopika -async def test_delay_router_rabbit(): - from docs.docs_src.getting_started.routers.rabbit.router_delay import ( - app, - broker, - ) - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker) as br, TestApp(app): - next(iter(br._subscribers.values())).calls[ - 0 - ].handler.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - next(iter(br._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_nats -async def test_delay_router_nats(): - from docs.docs_src.getting_started.routers.nats.router_delay import ( - app, - broker, - ) - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker) as br, TestApp(app): - next(iter(br._subscribers.values())).calls[ - 0 - ].handler.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - next(iter(br._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_redis -async def test_delay_router_redis(): - from docs.docs_src.getting_started.routers.redis.router_delay import ( - app, - broker, - ) - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker) as br, TestApp(app): - next(iter(br._subscribers.values())).calls[ - 0 - ].handler.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - next(iter(br._publishers.values())).mock.assert_called_once_with("Hi!") diff --git a/tests/a_docs/getting_started/routers/test_delay_equal.py b/tests/a_docs/getting_started/routers/test_delay_equal.py deleted file mode 100644 index 8e34b434fc..0000000000 --- a/tests/a_docs/getting_started/routers/test_delay_equal.py +++ /dev/null @@ -1,125 +0,0 @@ -import pytest - -from faststream import TestApp -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_delay_router_kafka(): - from docs.docs_src.getting_started.routers.kafka.delay_equal import ( - app, - broker, - ) - from docs.docs_src.getting_started.routers.kafka.router_delay import ( - broker as control_broker, - ) - from faststream.kafka import TestKafkaBroker - - assert broker._subscribers.keys() == control_broker._subscribers.keys() - assert broker._publishers.keys() == control_broker._publishers.keys() - - async with TestKafkaBroker(broker) as br, TestApp(app): - next(iter(br._subscribers.values())).calls[ - 0 - ].handler.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - next(iter(br._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_confluent -async def test_delay_router_confluent(): - from docs.docs_src.getting_started.routers.confluent.delay_equal import ( - app, - broker, - ) - from docs.docs_src.getting_started.routers.confluent.router_delay import ( - broker as control_broker, - ) - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - assert broker._subscribers.keys() == control_broker._subscribers.keys() - assert broker._publishers.keys() == control_broker._publishers.keys() - - async with TestConfluentKafkaBroker(broker) as br, TestApp(app): - next(iter(br._subscribers.values())).calls[ - 0 - ].handler.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - next(iter(br._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_aiopika -async def test_delay_router_rabbit(): - from docs.docs_src.getting_started.routers.rabbit.delay_equal import ( - app, - broker, - ) - from docs.docs_src.getting_started.routers.rabbit.router_delay import ( - broker as control_broker, - ) - from faststream.rabbit import TestRabbitBroker - - assert broker._subscribers.keys() == control_broker._subscribers.keys() - assert broker._publishers.keys() == control_broker._publishers.keys() - - async with TestRabbitBroker(broker) as br, TestApp(app): - next(iter(br._subscribers.values())).calls[ - 0 - ].handler.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - next(iter(br._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_nats -async def test_delay_router_nats(): - from docs.docs_src.getting_started.routers.nats.delay_equal import ( - app, - broker, - ) - from docs.docs_src.getting_started.routers.nats.router_delay import ( - broker as control_broker, - ) - from faststream.nats import TestNatsBroker - - assert broker._subscribers.keys() == control_broker._subscribers.keys() - assert broker._publishers.keys() == control_broker._publishers.keys() - - async with TestNatsBroker(broker) as br, TestApp(app): - next(iter(br._subscribers.values())).calls[ - 0 - ].handler.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - next(iter(br._publishers.values())).mock.assert_called_once_with("Hi!") - - -@pytest.mark.asyncio -@require_redis -async def test_delay_router_redis(): - from docs.docs_src.getting_started.routers.redis.delay_equal import ( - app, - broker, - ) - from docs.docs_src.getting_started.routers.redis.router_delay import ( - broker as control_broker, - ) - from faststream.redis import TestRedisBroker - - assert broker._subscribers.keys() == control_broker._subscribers.keys() - assert broker._publishers.keys() == control_broker._publishers.keys() - - async with TestRedisBroker(broker) as br, TestApp(app): - next(iter(br._subscribers.values())).calls[ - 0 - ].handler.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - next(iter(br._publishers.values())).mock.assert_called_once_with("Hi!") diff --git a/tests/a_docs/getting_started/subscription/test_annotated.py b/tests/a_docs/getting_started/subscription/test_annotated.py deleted file mode 100644 index e331c5f037..0000000000 --- a/tests/a_docs/getting_started/subscription/test_annotated.py +++ /dev/null @@ -1,98 +0,0 @@ -import pytest -from pydantic import ValidationError - -from tests.marks import ( - python39, - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@python39 -class BaseCase: - async def test_handle(self, setup): - broker, handle, test_class = setup - - async with test_class(broker) as br: - await br.publish({"name": "John", "user_id": 1}, "test") - handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - assert handle.mock is None - - async def test_validation_error(self, setup): - broker, handle, test_class = setup - - async with test_class(broker) as br: - with pytest.raises(ValidationError): - await br.publish("wrong message", "test") - - handle.mock.assert_called_once_with("wrong message") - - -@require_aiokafka -class TestKafka(BaseCase): - @pytest.fixture(scope="class") - def setup(self): - from docs.docs_src.getting_started.subscription.kafka.pydantic_annotated_fields import ( - broker, - handle, - ) - from faststream.kafka import TestKafkaBroker - - return (broker, handle, TestKafkaBroker) - - -@require_confluent -class TestConfluent(BaseCase): - @pytest.fixture(scope="class") - def setup(self): - from docs.docs_src.getting_started.subscription.confluent.pydantic_annotated_fields import ( - broker, - handle, - ) - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - return (broker, handle, TestConfluentKafkaBroker) - - -@require_aiopika -class TestRabbit(BaseCase): - @pytest.fixture(scope="class") - def setup(self): - from docs.docs_src.getting_started.subscription.rabbit.pydantic_annotated_fields import ( - broker, - handle, - ) - from faststream.rabbit import TestRabbitBroker - - return (broker, handle, TestRabbitBroker) - - -@require_nats -class TestNats(BaseCase): - @pytest.fixture(scope="class") - def setup(self): - from docs.docs_src.getting_started.subscription.nats.pydantic_annotated_fields import ( - broker, - handle, - ) - from faststream.nats import TestNatsBroker - - return (broker, handle, TestNatsBroker) - - -@require_redis -class TestRedis(BaseCase): - @pytest.fixture(scope="class") - def setup(self): - from docs.docs_src.getting_started.subscription.redis.pydantic_annotated_fields import ( - broker, - handle, - ) - from faststream.redis import TestRedisBroker - - return (broker, handle, TestRedisBroker) diff --git a/tests/a_docs/getting_started/subscription/test_pydantic.py b/tests/a_docs/getting_started/subscription/test_pydantic.py deleted file mode 100644 index 8a5ba2eac0..0000000000 --- a/tests/a_docs/getting_started/subscription/test_pydantic.py +++ /dev/null @@ -1,79 +0,0 @@ -import pytest - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiopika -async def test_pydantic_model_rabbit(): - from docs.docs_src.getting_started.subscription.rabbit.pydantic_model import ( - broker, - handle, - ) - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker) as br: - await br.publish({"name": "John", "user_id": 1}, "test-queue") - handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_pydantic_model_kafka(): - from docs.docs_src.getting_started.subscription.kafka.pydantic_model import ( - broker, - handle, - ) - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker) as br: - await br.publish({"name": "John", "user_id": 1}, "test-topic") - handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - -@pytest.mark.asyncio -@require_confluent -async def test_pydantic_model_confluent(): - from docs.docs_src.getting_started.subscription.confluent.pydantic_model import ( - broker, - handle, - ) - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker) as br: - await br.publish({"name": "John", "user_id": 1}, "test-topic") - handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - -@pytest.mark.asyncio -@require_nats -async def test_pydantic_model_nats(): - from docs.docs_src.getting_started.subscription.nats.pydantic_model import ( - broker, - handle, - ) - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker) as br: - await br.publish({"name": "John", "user_id": 1}, "test-subject") - handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) - - -@pytest.mark.asyncio -@require_redis -async def test_pydantic_model_redis(): - from docs.docs_src.getting_started.subscription.redis.pydantic_model import ( - broker, - handle, - ) - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker) as br: - await br.publish({"name": "John", "user_id": 1}, "test-channel") - handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) diff --git a/tests/a_docs/getting_started/subscription/test_real.py b/tests/a_docs/getting_started/subscription/test_real.py deleted file mode 100644 index 2862cfa5dc..0000000000 --- a/tests/a_docs/getting_started/subscription/test_real.py +++ /dev/null @@ -1,119 +0,0 @@ -import pytest - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.kafka -@pytest.mark.asyncio -@require_aiokafka -async def test_handle_kafka(): - from docs.docs_src.getting_started.subscription.kafka.real_testing import ( - test_handle as test_handle_k, - ) - - await test_handle_k() - - -@pytest.mark.kafka -@pytest.mark.asyncio -@require_aiokafka -async def test_validate_kafka(): - from docs.docs_src.getting_started.subscription.kafka.real_testing import ( - test_validation_error as test_validation_error_k, - ) - - await test_validation_error_k() - - -@pytest.mark.confluent -@pytest.mark.asyncio -@require_confluent -async def test_handle_confluent(): - from docs.docs_src.getting_started.subscription.confluent.real_testing import ( - test_handle as test_handle_confluent, - ) - - await test_handle_confluent() - - -@pytest.mark.asyncio -@pytest.mark.confluent -@require_confluent -async def test_validate_confluent(): - from docs.docs_src.getting_started.subscription.confluent.real_testing import ( - test_validation_error as test_validation_error_confluent, - ) - - await test_validation_error_confluent() - - -@pytest.mark.asyncio -@pytest.mark.rabbit -@require_aiopika -async def test_handle_rabbit(): - from docs.docs_src.getting_started.subscription.rabbit.real_testing import ( - test_handle as test_handle_r, - ) - - await test_handle_r() - - -@pytest.mark.asyncio -@pytest.mark.rabbit -@require_aiopika -async def test_validate_rabbit(): - from docs.docs_src.getting_started.subscription.rabbit.real_testing import ( - test_validation_error as test_validation_error_r, - ) - - await test_validation_error_r() - - -@pytest.mark.asyncio -@pytest.mark.nats -@require_nats -async def test_handle_nats(): - from docs.docs_src.getting_started.subscription.nats.real_testing import ( - test_handle as test_handle_n, - ) - - await test_handle_n() - - -@pytest.mark.asyncio -@pytest.mark.nats -@require_nats -async def test_validate_nats(): - from docs.docs_src.getting_started.subscription.nats.real_testing import ( - test_validation_error as test_validation_error_n, - ) - - await test_validation_error_n() - - -@pytest.mark.asyncio -@pytest.mark.redis -@require_redis -async def test_handle_redis(): - from docs.docs_src.getting_started.subscription.redis.real_testing import ( - test_handle as test_handle_red, - ) - - await test_handle_red() - - -@pytest.mark.asyncio -@pytest.mark.redis -@require_redis -async def test_validate_redis(): - from docs.docs_src.getting_started.subscription.redis.real_testing import ( - test_validation_error as test_validation_error_red, - ) - - await test_validation_error_red() diff --git a/tests/a_docs/getting_started/subscription/test_testing.py b/tests/a_docs/getting_started/subscription/test_testing.py deleted file mode 100644 index bb60dcc318..0000000000 --- a/tests/a_docs/getting_started/subscription/test_testing.py +++ /dev/null @@ -1,119 +0,0 @@ -import pytest - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.kafka -@pytest.mark.asyncio -@require_aiokafka -async def test_handle_kafka(): - from docs.docs_src.getting_started.subscription.kafka.testing import ( - test_handle as test_handle_k, - ) - - await test_handle_k() - - -@pytest.mark.kafka -@pytest.mark.asyncio -@require_aiokafka -async def test_validate_kafka(): - from docs.docs_src.getting_started.subscription.kafka.testing import ( - test_validation_error as test_validation_error_k, - ) - - await test_validation_error_k() - - -@pytest.mark.confluent -@pytest.mark.asyncio -@require_confluent -async def test_handle_confluent(): - from docs.docs_src.getting_started.subscription.confluent.testing import ( - test_handle as test_handle_confluent, - ) - - await test_handle_confluent() - - -@pytest.mark.asyncio -@pytest.mark.confluent -@require_confluent -async def test_validate_confluent(): - from docs.docs_src.getting_started.subscription.confluent.testing import ( - test_validation_error as test_validation_error_confluent, - ) - - await test_validation_error_confluent() - - -@pytest.mark.asyncio -@pytest.mark.rabbit -@require_aiopika -async def test_handle_rabbit(): - from docs.docs_src.getting_started.subscription.rabbit.testing import ( - test_handle as test_handle_r, - ) - - await test_handle_r() - - -@pytest.mark.asyncio -@pytest.mark.rabbit -@require_aiopika -async def test_validate_rabbit(): - from docs.docs_src.getting_started.subscription.rabbit.testing import ( - test_validation_error as test_validation_error_r, - ) - - await test_validation_error_r() - - -@pytest.mark.asyncio -@pytest.mark.nats -@require_nats -async def test_handle_nats(): - from docs.docs_src.getting_started.subscription.nats.testing import ( - test_handle as test_handle_n, - ) - - await test_handle_n() - - -@pytest.mark.asyncio -@pytest.mark.nats -@require_nats -async def test_validate_nats(): - from docs.docs_src.getting_started.subscription.nats.testing import ( - test_validation_error as test_validation_error_n, - ) - - await test_validation_error_n() - - -@pytest.mark.asyncio -@pytest.mark.redis -@require_redis -async def test_handle_redis(): - from docs.docs_src.getting_started.subscription.redis.testing import ( - test_handle as test_handle_rd, - ) - - await test_handle_rd() - - -@pytest.mark.asyncio -@pytest.mark.redis -@require_redis -async def test_validate_redis(): - from docs.docs_src.getting_started.subscription.redis.testing import ( - test_validation_error as test_validation_error_rd, - ) - - await test_validation_error_rd() diff --git a/tests/a_docs/index/test_basic.py b/tests/a_docs/index/test_basic.py deleted file mode 100644 index e56f220300..0000000000 --- a/tests/a_docs/index/test_basic.py +++ /dev/null @@ -1,89 +0,0 @@ -import pytest - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_index_kafka_base(): - from docs.docs_src.index.kafka.basic import broker, handle_msg - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker) as br: - await br.publish({"user": "John", "user_id": 1}, "in-topic") - - handle_msg.mock.assert_called_once_with({"user": "John", "user_id": 1}) - - list(br._publishers.values())[0].mock.assert_called_once_with( # noqa: RUF015 - "User: 1 - John registered" - ) - - -@pytest.mark.asyncio -@require_confluent -async def test_index_confluent_base(): - from docs.docs_src.index.confluent.basic import broker, handle_msg - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(broker) as br: - await br.publish({"user": "John", "user_id": 1}, "in-topic") - - handle_msg.mock.assert_called_once_with({"user": "John", "user_id": 1}) - - list(br._publishers.values())[0].mock.assert_called_once_with( # noqa: RUF015 - "User: 1 - John registered" - ) - - -@pytest.mark.asyncio -@require_aiopika -async def test_index_rabbit_base(): - from docs.docs_src.index.rabbit.basic import broker, handle_msg - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(broker) as br: - await br.publish({"user": "John", "user_id": 1}, "in-queue") - - handle_msg.mock.assert_called_once_with({"user": "John", "user_id": 1}) - - list(br._publishers.values())[0].mock.assert_called_once_with( # noqa: RUF015 - "User: 1 - John registered" - ) - - -@pytest.mark.asyncio -@require_nats -async def test_index_nats_base(): - from docs.docs_src.index.nats.basic import broker, handle_msg - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(broker) as br: - await br.publish({"user": "John", "user_id": 1}, "in-subject") - - handle_msg.mock.assert_called_once_with({"user": "John", "user_id": 1}) - - list(br._publishers.values())[0].mock.assert_called_once_with( # noqa: RUF015 - "User: 1 - John registered" - ) - - -@pytest.mark.asyncio -@require_redis -async def test_index_redis_base(): - from docs.docs_src.index.redis.basic import broker, handle_msg - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(broker) as br: - await br.publish({"user": "John", "user_id": 1}, "in-channel") - - handle_msg.mock.assert_called_once_with({"user": "John", "user_id": 1}) - - list(br._publishers.values())[0].mock.assert_called_once_with( # noqa: RUF015 - "User: 1 - John registered" - ) diff --git a/tests/a_docs/index/test_pydantic.py b/tests/a_docs/index/test_pydantic.py deleted file mode 100644 index 977ff484c9..0000000000 --- a/tests/a_docs/index/test_pydantic.py +++ /dev/null @@ -1,93 +0,0 @@ -import pytest - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_kafka_correct(): - from docs.docs_src.index.kafka.test import test_correct as test_k_correct - - await test_k_correct() - - -@pytest.mark.asyncio -@require_aiokafka -async def test_kafka_invalid(): - from docs.docs_src.index.kafka.test import test_invalid as test_k_invalid - - await test_k_invalid() - - -@pytest.mark.asyncio -@require_confluent -async def test_confluent_correct(): - from docs.docs_src.index.confluent.test import ( - test_correct as test_confluent_correct, - ) - - await test_confluent_correct() - - -@pytest.mark.asyncio -@require_confluent -async def test_confluent_invalid(): - from docs.docs_src.index.confluent.test import ( - test_invalid as test_confluent_invalid, - ) - - await test_confluent_invalid() - - -@pytest.mark.asyncio -@require_aiopika -async def test_rabbit_correct(): - from docs.docs_src.index.rabbit.test import test_correct as test_r_correct - - await test_r_correct() - - -@pytest.mark.asyncio -@require_aiopika -async def test_rabbit_invalid(): - from docs.docs_src.index.rabbit.test import test_invalid as test_r_invalid - - await test_r_invalid() - - -@pytest.mark.asyncio -@require_nats -async def test_nats_correct(): - from docs.docs_src.index.nats.test import test_correct as test_n_correct - - await test_n_correct() - - -@pytest.mark.asyncio -@require_nats -async def test_nats_invalid(): - from docs.docs_src.index.nats.test import test_invalid as test_n_invalid - - await test_n_invalid() - - -@pytest.mark.asyncio -@require_redis -async def test_redis_correct(): - from docs.docs_src.index.redis.test import test_correct as test_red_correct - - await test_red_correct() - - -@pytest.mark.asyncio -@require_redis -async def test_redis_invalid(): - from docs.docs_src.index.redis.test import test_invalid as test_red_invalid - - await test_red_invalid() diff --git a/tests/a_docs/integration/fastapi/test_base.py b/tests/a_docs/integration/fastapi/test_base.py deleted file mode 100644 index 205db8d785..0000000000 --- a/tests/a_docs/integration/fastapi/test_base.py +++ /dev/null @@ -1,105 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_fastapi_kafka_base(): - from docs.docs_src.integrations.fastapi.kafka.base import app, hello, router - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(router.broker) as br: - with TestClient(app) as client: - assert client.get("/").text == '"Hello, HTTP!"' - - await br.publish({"m": {}}, "test") - - hello.mock.assert_called_once_with({"m": {}}) - - list(br._publishers.values())[0].mock.assert_called_with( # noqa: RUF015 - {"response": "Hello, Kafka!"} - ) - - -@pytest.mark.asyncio -@require_confluent -async def test_fastapi_confluent_base(): - from docs.docs_src.integrations.fastapi.confluent.base import app, hello, router - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - async with TestConfluentKafkaBroker(router.broker) as br: - with TestClient(app) as client: - assert client.get("/").text == '"Hello, HTTP!"' - - await br.publish({"m": {}}, "test") - - hello.mock.assert_called_once_with({"m": {}}) - - list(br._publishers.values())[0].mock.assert_called_with( # noqa: RUF015 - {"response": "Hello, Kafka!"} - ) - - -@pytest.mark.asyncio -@require_aiopika -async def test_fastapi_rabbit_base(): - from docs.docs_src.integrations.fastapi.rabbit.base import app, hello, router - from faststream.rabbit import TestRabbitBroker - - async with TestRabbitBroker(router.broker) as br: - with TestClient(app) as client: - assert client.get("/").text == '"Hello, HTTP!"' - - await br.publish({"m": {}}, "test") - - hello.mock.assert_called_once_with({"m": {}}) - - list(br._publishers.values())[0].mock.assert_called_with( # noqa: RUF015 - {"response": "Hello, Rabbit!"} - ) - - -@pytest.mark.asyncio -@require_nats -async def test_fastapi_nats_base(): - from docs.docs_src.integrations.fastapi.nats.base import app, hello, router - from faststream.nats import TestNatsBroker - - async with TestNatsBroker(router.broker) as br: - with TestClient(app) as client: - assert client.get("/").text == '"Hello, HTTP!"' - - await br.publish({"m": {}}, "test") - - hello.mock.assert_called_once_with({"m": {}}) - - list(br._publishers.values())[0].mock.assert_called_with( # noqa: RUF015 - {"response": "Hello, NATS!"} - ) - - -@pytest.mark.asyncio -@require_redis -async def test_fastapi_redis_base(): - from docs.docs_src.integrations.fastapi.redis.base import app, hello, router - from faststream.redis import TestRedisBroker - - async with TestRedisBroker(router.broker) as br: - with TestClient(app) as client: - assert client.get("/").text == '"Hello, HTTP!"' - - await br.publish({"m": {}}, "test") - - hello.mock.assert_called_once_with({"m": {}}) - - list(br._publishers.values())[0].mock.assert_called_with( # noqa: RUF015 - {"response": "Hello, Redis!"} - ) diff --git a/tests/a_docs/integration/fastapi/test_depends.py b/tests/a_docs/integration/fastapi/test_depends.py deleted file mode 100644 index 0f798fd922..0000000000 --- a/tests/a_docs/integration/fastapi/test_depends.py +++ /dev/null @@ -1,90 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_fastapi_kafka_depends(): - from docs.docs_src.integrations.fastapi.kafka.depends import app, router - from faststream.kafka import TestKafkaBroker - - @router.subscriber("test") - async def handler(): ... - - async with TestKafkaBroker(router.broker): - with TestClient(app) as client: - assert client.get("/").text == '"Hello, HTTP!"' - - handler.mock.assert_called_once_with("Hello, Kafka!") - - -@pytest.mark.asyncio -@require_confluent -async def test_fastapi_confluent_depends(): - from docs.docs_src.integrations.fastapi.confluent.depends import app, router - from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker - - @router.subscriber("test") - async def handler(): ... - - async with TestConfluentKafkaBroker(router.broker): - with TestClient(app) as client: - assert client.get("/").text == '"Hello, HTTP!"' - - handler.mock.assert_called_once_with("Hello, Kafka!") - - -@pytest.mark.asyncio -@require_aiopika -async def test_fastapi_rabbit_depends(): - from docs.docs_src.integrations.fastapi.rabbit.depends import app, router - from faststream.rabbit import TestRabbitBroker - - @router.subscriber("test") - async def handler(): ... - - async with TestRabbitBroker(router.broker): - with TestClient(app) as client: - assert client.get("/").text == '"Hello, HTTP!"' - - handler.mock.assert_called_once_with("Hello, Rabbit!") - - -@pytest.mark.asyncio -@require_nats -async def test_fastapi_nats_depends(): - from docs.docs_src.integrations.fastapi.nats.depends import app, router - from faststream.nats import TestNatsBroker - - @router.subscriber("test") - async def handler(): ... - - async with TestNatsBroker(router.broker): - with TestClient(app) as client: - assert client.get("/").text == '"Hello, HTTP!"' - - handler.mock.assert_called_once_with("Hello, NATS!") - - -@pytest.mark.asyncio -@require_redis -async def test_fastapi_redis_depends(): - from docs.docs_src.integrations.fastapi.redis.depends import app, router - from faststream.redis import TestRedisBroker - - @router.subscriber("test") - async def handler(): ... - - async with TestRedisBroker(router.broker): - with TestClient(app) as client: - assert client.get("/").text == '"Hello, HTTP!"' - - handler.mock.assert_called_once_with("Hello, Redis!") diff --git a/tests/a_docs/integration/fastapi/test_test.py b/tests/a_docs/integration/fastapi/test_test.py deleted file mode 100644 index 1e79da205c..0000000000 --- a/tests/a_docs/integration/fastapi/test_test.py +++ /dev/null @@ -1,49 +0,0 @@ -import pytest - -from tests.marks import ( - require_aiokafka, - require_aiopika, - require_confluent, - require_nats, - require_redis, -) - - -@pytest.mark.asyncio -@require_aiokafka -async def test_kafka(): - from docs.docs_src.integrations.fastapi.kafka.test import test_router - - await test_router() - - -@pytest.mark.asyncio -@require_confluent -async def test_confluent(): - from docs.docs_src.integrations.fastapi.confluent.test import test_router - - await test_router() - - -@pytest.mark.asyncio -@require_aiopika -async def test_rabbit(): - from docs.docs_src.integrations.fastapi.rabbit.test import test_router - - await test_router() - - -@pytest.mark.asyncio -@require_nats -async def test_nats(): - from docs.docs_src.integrations.fastapi.nats.test import test_router - - await test_router() - - -@pytest.mark.asyncio -@require_redis -async def test_redis(): - from docs.docs_src.integrations.fastapi.redis.test import test_router - - await test_router() diff --git a/tests/a_docs/integration/http/test_fastapi.py b/tests/a_docs/integration/http/test_fastapi.py deleted file mode 100644 index b257f92501..0000000000 --- a/tests/a_docs/integration/http/test_fastapi.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from tests.marks import require_aiokafka - - -@pytest.mark.asyncio -@require_aiokafka -async def test_fastapi_raw_integration(): - from docs.docs_src.integrations.http_frameworks_integrations.fastapi import ( - app, - base_handler, - broker, - ) - from faststream.kafka import TestKafkaBroker - - async with TestKafkaBroker(broker): - with TestClient(app) as client: - assert client.get("/").json() == {"Hello": "World"} - - await broker.publish("", "test") - - base_handler.mock.assert_called_once_with("") diff --git a/tests/a_docs/kafka/ack/test_errors.py b/tests/a_docs/kafka/ack/test_errors.py deleted file mode 100644 index a17bb1ad46..0000000000 --- a/tests/a_docs/kafka/ack/test_errors.py +++ /dev/null @@ -1,22 +0,0 @@ -from unittest.mock import patch - -import pytest -from aiokafka import AIOKafkaConsumer - -from faststream.kafka import TestApp, TestKafkaBroker -from tests.tools import spy_decorator - - -@pytest.mark.asyncio -@pytest.mark.kafka -@pytest.mark.slow -async def test_ack_exc(): - from docs.docs_src.kafka.ack.errors import app, broker, handle - - with patch.object( - AIOKafkaConsumer, "commit", spy_decorator(AIOKafkaConsumer.commit) - ) as m: - async with TestKafkaBroker(broker, with_real=True), TestApp(app): - await handle.wait_call(10) - - assert m.mock.call_count diff --git a/tests/a_docs/kafka/basic/test_basic.py b/tests/a_docs/kafka/basic/test_basic.py deleted file mode 100644 index 624cb73ca2..0000000000 --- a/tests/a_docs/kafka/basic/test_basic.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from faststream.kafka import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_basic(): - from docs.docs_src.kafka.basic.basic import broker, on_input_data - - publisher = list(broker._publishers.values())[0] # noqa: RUF015 - - async with TestKafkaBroker(broker) as br: - await br.publish({"data": 1.0}, "input_data") - on_input_data.mock.assert_called_once_with({"data": 1.0}) - publisher.mock.assert_called_once_with({"data": 2.0}) diff --git a/tests/a_docs/kafka/basic/test_cmd_run.py b/tests/a_docs/kafka/basic/test_cmd_run.py deleted file mode 100644 index 0d7609faf6..0000000000 --- a/tests/a_docs/kafka/basic/test_cmd_run.py +++ /dev/null @@ -1,35 +0,0 @@ -import asyncio -from unittest.mock import Mock - -import pytest -from typer.testing import CliRunner - -from faststream.app import FastStream -from faststream.cli.main import cli - - -@pytest.mark.kafka -def test_run_cmd( - runner: CliRunner, - mock: Mock, - event: asyncio.Event, - monkeypatch: pytest.MonkeyPatch, - kafka_basic_project, -): - async def patched_run(self: FastStream, *args, **kwargs): - await self.start() - await self.stop() - mock() - - with monkeypatch.context() as m: - m.setattr(FastStream, "run", patched_run) - r = runner.invoke( - cli, - [ - "run", - kafka_basic_project, - ], - ) - - assert r.exit_code == 0 - mock.assert_called_once() diff --git a/tests/a_docs/kafka/batch_consuming_pydantic/test_app.py b/tests/a_docs/kafka/batch_consuming_pydantic/test_app.py deleted file mode 100644 index 4b39b471f5..0000000000 --- a/tests/a_docs/kafka/batch_consuming_pydantic/test_app.py +++ /dev/null @@ -1,21 +0,0 @@ -import pytest - -from docs.docs_src.kafka.batch_consuming_pydantic.app import ( - HelloWorld, - broker, - handle_batch, -) -from faststream.kafka import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_me(): - async with TestKafkaBroker(broker): - await broker.publish_batch( - HelloWorld(msg="First Hello"), - HelloWorld(msg="Second Hello"), - topic="test_batch", - ) - handle_batch.mock.assert_called_with( - [dict(HelloWorld(msg="First Hello")), dict(HelloWorld(msg="Second Hello"))] - ) diff --git a/tests/a_docs/kafka/consumes_basics/test_app.py b/tests/a_docs/kafka/consumes_basics/test_app.py deleted file mode 100644 index bcf5c9f630..0000000000 --- a/tests/a_docs/kafka/consumes_basics/test_app.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from docs.docs_src.kafka.consumes_basics.app import ( - HelloWorld, - broker, - on_hello_world, -) -from faststream.kafka import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_base_app(): - async with TestKafkaBroker(broker): - await broker.publish(HelloWorld(msg="First Hello"), "hello_world") - on_hello_world.mock.assert_called_with(dict(HelloWorld(msg="First Hello"))) diff --git a/tests/a_docs/kafka/publish_batch/test_app.py b/tests/a_docs/kafka/publish_batch/test_app.py deleted file mode 100644 index 99bf043700..0000000000 --- a/tests/a_docs/kafka/publish_batch/test_app.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest - -from docs.docs_src.kafka.publish_batch.app import ( - Data, - broker, - decrease_and_increase, - on_input_data_1, - on_input_data_2, -) -from faststream.kafka import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_batch_publish_decorator(): - async with TestKafkaBroker(broker): - await broker.publish(Data(data=2.0), "input_data_1") - - on_input_data_1.mock.assert_called_once_with(dict(Data(data=2.0))) - decrease_and_increase.mock.assert_called_once_with( - [dict(Data(data=1.0)), dict(Data(data=4.0))] - ) - - -@pytest.mark.asyncio -async def test_batch_publish_call(): - async with TestKafkaBroker(broker): - await broker.publish(Data(data=2.0), "input_data_2") - - on_input_data_2.mock.assert_called_once_with(dict(Data(data=2.0))) - decrease_and_increase.mock.assert_called_once_with( - [dict(Data(data=1.0)), dict(Data(data=4.0))] - ) diff --git a/tests/a_docs/kafka/publish_batch/test_issues.py b/tests/a_docs/kafka/publish_batch/test_issues.py deleted file mode 100644 index 65526eaeee..0000000000 --- a/tests/a_docs/kafka/publish_batch/test_issues.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import List - -import pytest - -from faststream import FastStream -from faststream.kafka import KafkaBroker, TestKafkaBroker - -broker = KafkaBroker() -batch_producer = broker.publisher("response", batch=True) - - -@batch_producer -@broker.subscriber("test") -async def handle(msg: str) -> List[int]: - return [1, 2, 3] - - -app = FastStream(broker) - - -@pytest.mark.asyncio -async def test_base_app(): - async with TestKafkaBroker(broker): - await broker.publish("", "test") diff --git a/tests/a_docs/kafka/publish_example/test_app.py b/tests/a_docs/kafka/publish_example/test_app.py deleted file mode 100644 index 659e6c19a1..0000000000 --- a/tests/a_docs/kafka/publish_example/test_app.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -from docs.docs_src.kafka.publish_example.app import ( - Data, - broker, - on_input_data, - to_output_data, -) -from faststream.kafka import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_base_app(): - async with TestKafkaBroker(broker): - await broker.publish(Data(data=0.2), "input_data") - - on_input_data.mock.assert_called_once_with(dict(Data(data=0.2))) - to_output_data.mock.assert_called_once_with(dict(Data(data=1.2))) diff --git a/tests/a_docs/kafka/publish_with_partition_key/test_app.py b/tests/a_docs/kafka/publish_with_partition_key/test_app.py deleted file mode 100644 index 8cf871f80c..0000000000 --- a/tests/a_docs/kafka/publish_with_partition_key/test_app.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest - -from docs.docs_src.kafka.publish_with_partition_key.app import ( - Data, - broker, - on_input_data, - to_output_data, -) -from faststream.kafka import TestKafkaBroker - - -@pytest.mark.asyncio -async def test_app(): - async with TestKafkaBroker(broker): - await broker.publish(Data(data=0.2), "input_data", key=b"my_key") - - on_input_data.mock.assert_called_once_with(dict(Data(data=0.2))) - to_output_data.mock.assert_called_once_with(dict(Data(data=1.2))) - - -@pytest.mark.skip("we are not checking the key") -@pytest.mark.asyncio -async def test_keys(): - async with TestKafkaBroker(broker): - # we should be able to publish a message with the key - await broker.publish(Data(data=0.2), "input_data", key=b"my_key") - - # we need to check the key as well - on_input_data.mock.assert_called_once_with(dict(Data(data=0.2)), key=b"my_key") - to_output_data.mock.assert_called_once_with(dict(Data(data=1.2)), key=b"key") diff --git a/tests/a_docs/kafka/test_security.py b/tests/a_docs/kafka/test_security.py deleted file mode 100644 index e693575bc9..0000000000 --- a/tests/a_docs/kafka/test_security.py +++ /dev/null @@ -1,125 +0,0 @@ -import ssl -from contextlib import contextmanager -from typing import Tuple -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest - - -@contextmanager -def patch_aio_consumer_and_producer() -> Tuple[MagicMock, MagicMock]: - try: - producer = MagicMock(return_value=AsyncMock()) - admin_client = MagicMock(return_value=AsyncMock()) - - with ( - patch("aiokafka.AIOKafkaProducer", new=producer), - patch("aiokafka.admin.client.AIOKafkaAdminClient", new=admin_client), - ): - yield producer - finally: - pass - - -@pytest.mark.asyncio -@pytest.mark.kafka -async def test_base_security(): - from docs.docs_src.kafka.security.basic import broker as basic_broker - - with patch_aio_consumer_and_producer() as producer: - async with basic_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = {} - call_kwargs["security_protocol"] = "SSL" - - assert call_kwargs.items() <= producer_call_kwargs.items() - - assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext - - -@pytest.mark.asyncio -@pytest.mark.kafka -async def test_scram256(): - from docs.docs_src.kafka.security.sasl_scram256 import ( - broker as scram256_broker, - ) - - with patch_aio_consumer_and_producer() as producer: - async with scram256_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = {} - call_kwargs["sasl_mechanism"] = "SCRAM-SHA-256" - call_kwargs["sasl_plain_username"] = "admin" - call_kwargs["sasl_plain_password"] = "password" # pragma: allowlist secret - call_kwargs["security_protocol"] = "SASL_SSL" - - assert call_kwargs.items() <= producer_call_kwargs.items() - - assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext - - -@pytest.mark.asyncio -@pytest.mark.kafka -async def test_scram512(): - from docs.docs_src.kafka.security.sasl_scram512 import ( - broker as scram512_broker, - ) - - with patch_aio_consumer_and_producer() as producer: - async with scram512_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = {} - call_kwargs["sasl_mechanism"] = "SCRAM-SHA-512" - call_kwargs["sasl_plain_username"] = "admin" - call_kwargs["sasl_plain_password"] = "password" # pragma: allowlist secret - call_kwargs["security_protocol"] = "SASL_SSL" - - assert call_kwargs.items() <= producer_call_kwargs.items() - - assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext - - -@pytest.mark.asyncio -@pytest.mark.kafka -async def test_plaintext(): - from docs.docs_src.kafka.security.plaintext import ( - broker as plaintext_broker, - ) - - with patch_aio_consumer_and_producer() as producer: - async with plaintext_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = {} - call_kwargs["sasl_mechanism"] = "PLAIN" - call_kwargs["sasl_plain_username"] = "admin" - call_kwargs["sasl_plain_password"] = "password" # pragma: allowlist secret - call_kwargs["security_protocol"] = "SASL_SSL" - - assert call_kwargs.items() <= producer_call_kwargs.items() - - assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext - - -@pytest.mark.kafka -@pytest.mark.asyncio -async def test_gssapi(): - from docs.docs_src.kafka.security.sasl_gssapi import ( - broker as gssapi_broker, - ) - - with patch_aio_consumer_and_producer() as producer: - async with gssapi_broker: - producer_call_kwargs = producer.call_args.kwargs - - call_kwargs = { - "sasl_mechanism": "GSSAPI", - "security_protocol": "SASL_SSL", - } - - assert call_kwargs.items() <= producer_call_kwargs.items() - - assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext diff --git a/tests/a_docs/nats/ack/test_errors.py b/tests/a_docs/nats/ack/test_errors.py deleted file mode 100644 index 32e4379c15..0000000000 --- a/tests/a_docs/nats/ack/test_errors.py +++ /dev/null @@ -1,19 +0,0 @@ -from unittest.mock import patch - -import pytest -from nats.aio.msg import Msg - -from faststream.nats import TestApp, TestNatsBroker -from tests.tools import spy_decorator - - -@pytest.mark.asyncio -@pytest.mark.nats -async def test_ack_exc(): - from docs.docs_src.nats.ack.errors import app, broker, handle - - with patch.object(Msg, "ack", spy_decorator(Msg.ack)) as m: - async with TestNatsBroker(broker, with_real=True), TestApp(app): - await handle.wait_call(3) - - assert m.mock.call_count diff --git a/tests/a_docs/nats/js/test_kv.py b/tests/a_docs/nats/js/test_kv.py deleted file mode 100644 index 791db040cd..0000000000 --- a/tests/a_docs/nats/js/test_kv.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest - -from faststream import TestApp -from faststream.nats import TestNatsBroker - - -@pytest.mark.asyncio -@pytest.mark.nats -async def test_basic(): - from docs.docs_src.nats.js.key_value import app, broker, handler - - async with TestNatsBroker(broker, with_real=True), TestApp(app): - await handler.wait_call(3.0) - handler.mock.assert_called_once_with(b"Hello!") diff --git a/tests/a_docs/nats/js/test_object.py b/tests/a_docs/nats/js/test_object.py deleted file mode 100644 index 10f9fd99f8..0000000000 --- a/tests/a_docs/nats/js/test_object.py +++ /dev/null @@ -1,17 +0,0 @@ -import pytest - -from faststream import TestApp -from faststream.nats import TestNatsBroker - - -@pytest.mark.asyncio -@pytest.mark.nats -async def test_basic(): - from docs.docs_src.nats.js.object import app, broker, handler - - async with ( - TestNatsBroker(broker, with_real=True, connect_only=True), - TestApp(app), - ): - await handler.wait_call(3.0) - handler.mock.assert_called_once_with("file.txt") diff --git a/tests/a_docs/nats/test_direct.py b/tests/a_docs/nats/test_direct.py deleted file mode 100644 index d64e849fc8..0000000000 --- a/tests/a_docs/nats/test_direct.py +++ /dev/null @@ -1,19 +0,0 @@ -import pytest - -from faststream.nats import TestApp, TestNatsBroker - - -@pytest.mark.asyncio -async def test_pattern(): - from docs.docs_src.nats.direct import ( - app, - base_handler1, - base_handler2, - base_handler3, - broker, - ) - - async with TestNatsBroker(broker), TestApp(app): - assert base_handler1.mock.call_count == 2 - assert base_handler2.mock.call_count == 0 - assert base_handler3.mock.call_count == 1 diff --git a/tests/a_docs/rabbit/ack/test_errors.py b/tests/a_docs/rabbit/ack/test_errors.py deleted file mode 100644 index 8e8f98e3c7..0000000000 --- a/tests/a_docs/rabbit/ack/test_errors.py +++ /dev/null @@ -1,19 +0,0 @@ -from unittest.mock import patch - -import pytest -from aio_pika import IncomingMessage - -from faststream.rabbit import TestApp, TestRabbitBroker -from tests.tools import spy_decorator - - -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_ack_exc(): - from docs.docs_src.rabbit.ack.errors import app, broker, handle - - with patch.object(IncomingMessage, "ack", spy_decorator(IncomingMessage.ack)) as m: - async with TestRabbitBroker(broker, with_real=True), TestApp(app): - await handle.wait_call(3) - - m.mock.assert_called_once() diff --git a/tests/a_docs/rabbit/subscription/test_direct.py b/tests/a_docs/rabbit/subscription/test_direct.py deleted file mode 100644 index aa13430de8..0000000000 --- a/tests/a_docs/rabbit/subscription/test_direct.py +++ /dev/null @@ -1,17 +0,0 @@ -import pytest - -from faststream.rabbit import TestApp, TestRabbitBroker - - -@pytest.mark.asyncio -async def test_index(): - from docs.docs_src.rabbit.subscription.direct import ( - app, - base_handler1, - base_handler3, - broker, - ) - - async with TestRabbitBroker(broker), TestApp(app): - base_handler1.mock.assert_called_with(b"") - base_handler3.mock.assert_called_once_with(b"") diff --git a/tests/a_docs/rabbit/test_bind.py b/tests/a_docs/rabbit/test_bind.py deleted file mode 100644 index d2656a6f5c..0000000000 --- a/tests/a_docs/rabbit/test_bind.py +++ /dev/null @@ -1,30 +0,0 @@ -from unittest.mock import AsyncMock - -import pytest -from aio_pika import RobustQueue - -from faststream import TestApp -from tests.marks import require_aiopika - - -@pytest.mark.asyncio -@pytest.mark.rabbit -@require_aiopika -async def test_bind(monkeypatch, async_mock: AsyncMock): - from docs.docs_src.rabbit.bind import app, broker, some_exchange, some_queue - - with monkeypatch.context() as m: - m.setattr(RobustQueue, "bind", async_mock) - - async with TestApp(app): - assert len(broker.declarer._RabbitDeclarer__queues) == 2 # with `reply-to` - assert len(broker.declarer._RabbitDeclarer__exchanges) == 1 - - assert some_queue in broker.declarer._RabbitDeclarer__queues - assert some_exchange in broker.declarer._RabbitDeclarer__exchanges - - row_exchange = await broker.declarer.declare_exchange(some_exchange) - async_mock.assert_awaited_once_with( - exchange=row_exchange, - routing_key=some_queue.name, - ) diff --git a/tests/a_docs/rabbit/test_declare.py b/tests/a_docs/rabbit/test_declare.py deleted file mode 100644 index d3720a1171..0000000000 --- a/tests/a_docs/rabbit/test_declare.py +++ /dev/null @@ -1,13 +0,0 @@ -import pytest - -from faststream import TestApp - - -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_declare(): - from docs.docs_src.rabbit.declare import app, broker - - async with TestApp(app): - assert len(broker.declarer._RabbitDeclarer__exchanges) == 1 - assert len(broker.declarer._RabbitDeclarer__queues) == 2 # with `reply-to` diff --git a/tests/a_docs/rabbit/test_security.py b/tests/a_docs/rabbit/test_security.py deleted file mode 100644 index 30572bf947..0000000000 --- a/tests/a_docs/rabbit/test_security.py +++ /dev/null @@ -1,66 +0,0 @@ -import pytest -from aiormq.exceptions import AMQPConnectionError - -from faststream.app import FastStream -from faststream.asyncapi.generate import get_app_schema - - -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_base_security(): - from docs.docs_src.rabbit.security.basic import broker - - with pytest.raises(AMQPConnectionError): - async with broker: - pass - - schema = get_app_schema(FastStream(broker)).to_jsonable() - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}, "securitySchemes": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "amqps", - "protocolVersion": "0.9.1", - "security": [], - "url": "amqps://guest:guest@localhost:5672/", # pragma: allowlist secret - } - }, - } - - -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_plaintext_security(): - from docs.docs_src.rabbit.security.plaintext import broker - - with pytest.raises(AMQPConnectionError): - async with broker: - pass - - schema = get_app_schema(FastStream(broker)).to_jsonable() - assert ( - schema - == { - "asyncapi": "2.6.0", - "channels": {}, - "components": { - "messages": {}, - "schemas": {}, - "securitySchemes": {"user-password": {"type": "userPassword"}}, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "amqps", - "protocolVersion": "0.9.1", - "security": [{"user-password": []}], - "url": "amqps://admin:password@localhost:5672/", # pragma: allowlist secret - } - }, - } - ) diff --git a/tests/a_docs/redis/list/test_list_pub.py b/tests/a_docs/redis/list/test_list_pub.py deleted file mode 100644 index 0ef35761b4..0000000000 --- a/tests/a_docs/redis/list/test_list_pub.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from faststream.redis import TestRedisBroker - - -@pytest.mark.asyncio -async def test_list_publisher(): - from docs.docs_src.redis.list.list_pub import broker, on_input_data - - publisher = list(broker._publishers.values())[0] # noqa: RUF015 - - async with TestRedisBroker(broker) as br: - await br.publish({"data": 1.0}, list="input-list") - on_input_data.mock.assert_called_once_with({"data": 1.0}) - publisher.mock.assert_called_once_with({"data": 2.0}) diff --git a/tests/a_docs/redis/stream/test_batch_sub.py b/tests/a_docs/redis/stream/test_batch_sub.py deleted file mode 100644 index 24908211dc..0000000000 --- a/tests/a_docs/redis/stream/test_batch_sub.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest - -from faststream.redis import TestRedisBroker -from tests.marks import python39 - - -@pytest.mark.asyncio -@python39 -async def test_stream_batch(): - from docs.docs_src.redis.stream.batch_sub import broker, handle - - async with TestRedisBroker(broker) as br: - await br.publish("Hi!", stream="test-stream") - handle.mock.assert_called_once_with(["Hi!"]) diff --git a/tests/a_docs/redis/test_pipeline.py b/tests/a_docs/redis/test_pipeline.py deleted file mode 100644 index bec511caa6..0000000000 --- a/tests/a_docs/redis/test_pipeline.py +++ /dev/null @@ -1,17 +0,0 @@ -import pytest - -from faststream.redis import TestRedisBroker -from faststream.testing.app import TestApp - - -@pytest.mark.asyncio -async def test_pipeline(): - from docs.docs_src.redis.pipeline.pipeline import ( - app, - broker, - handle, - ) - - broker._is_validate = False - async with TestRedisBroker(broker), TestApp(app): - handle.mock.assert_called_once_with("Hi!") diff --git a/tests/a_docs/redis/test_rpc.py b/tests/a_docs/redis/test_rpc.py deleted file mode 100644 index 5f8a7ca580..0000000000 --- a/tests/a_docs/redis/test_rpc.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest - -from faststream.redis import TestApp, TestRedisBroker - - -@pytest.mark.asyncio -async def test_rpc(): - from docs.docs_src.redis.rpc.app import ( - app, - broker, - ) - - async with TestRedisBroker(broker), TestApp(app): - pass diff --git a/tests/a_docs/redis/test_security.py b/tests/a_docs/redis/test_security.py deleted file mode 100644 index 1b7efecd3f..0000000000 --- a/tests/a_docs/redis/test_security.py +++ /dev/null @@ -1,91 +0,0 @@ -from contextlib import contextmanager -from typing import Tuple -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -from redis.exceptions import AuthenticationError - -from faststream.app import FastStream -from faststream.asyncapi.generate import get_app_schema - - -@contextmanager -def patch_asyncio_open_connection() -> Tuple[MagicMock, MagicMock]: - try: - reader = MagicMock() - reader.readline = AsyncMock(return_value=b":1\r\n") - reader.read = AsyncMock(return_value=b"") - - writer = MagicMock() - writer.drain = AsyncMock() - writer.wait_closed = AsyncMock() - - open_connection = AsyncMock(return_value=(reader, writer)) - - with patch("asyncio.open_connection", new=open_connection): - yield open_connection - finally: - pass - - -@pytest.mark.asyncio -@pytest.mark.redis -async def test_base_security(): - with patch_asyncio_open_connection() as connection: - from docs.docs_src.redis.security.basic import broker - - async with broker: - await broker.ping(3.0) - - assert connection.call_args.kwargs["ssl"] - - schema = get_app_schema(FastStream(broker)).to_jsonable() - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}, "securitySchemes": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "redis", - "protocolVersion": "custom", - "security": [], - "url": "redis://localhost:6379", - } - }, - } - - -@pytest.mark.asyncio -@pytest.mark.redis -async def test_plaintext_security(): - with patch_asyncio_open_connection() as connection: - from docs.docs_src.redis.security.plaintext import broker - - with pytest.raises(AuthenticationError): - async with broker: - await broker._connection.ping() - - assert connection.call_args.kwargs["ssl"] - - schema = get_app_schema(FastStream(broker)).to_jsonable() - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": { - "messages": {}, - "schemas": {}, - "securitySchemes": {"user-password": {"type": "userPassword"}}, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "redis", - "protocolVersion": "custom", - "security": [{"user-password": []}], - "url": "redis://localhost:6379", - } - }, - } diff --git a/faststream/cli/supervisors/__init__.py b/tests/application/__init__.py similarity index 100% rename from faststream/cli/supervisors/__init__.py rename to tests/application/__init__.py diff --git a/tests/application/test_delayed_broker.py b/tests/application/test_delayed_broker.py new file mode 100644 index 0000000000..c2eb32cf57 --- /dev/null +++ b/tests/application/test_delayed_broker.py @@ -0,0 +1,38 @@ +import pytest + +from faststream._internal.application import StartAbleApplication +from faststream.exceptions import SetupError +from faststream.rabbit import RabbitBroker + + +def test_set_broker() -> None: + app = StartAbleApplication() + + assert app.broker is None + + broker = RabbitBroker() + app.set_broker(broker) + + assert app.broker is broker + + +def test_set_more_than_once_broker() -> None: + app = StartAbleApplication() + broker_1 = RabbitBroker() + broker_2 = RabbitBroker() + + app.set_broker(broker_1) + + with pytest.raises( + SetupError, + match=f"`{app}` already has a broker. You can't use multiple brokers until 1.0.0 release.", + ): + app.set_broker(broker_2) + + +@pytest.mark.asyncio() +async def test_start_not_setup_broker() -> None: + app = StartAbleApplication() + + with pytest.raises(AssertionError, match="You should setup a broker"): + await app._start_broker() diff --git a/tests/asgi/confluent/test_asgi.py b/tests/asgi/confluent/test_asgi.py index 75e4b37254..bd37b1d58f 100644 --- a/tests/asgi/confluent/test_asgi.py +++ b/tests/asgi/confluent/test_asgi.py @@ -3,8 +3,8 @@ class TestConfluentAsgi(AsgiTestcase): - def get_broker(self): - return KafkaBroker() + def get_broker(self, **kwargs) -> KafkaBroker: + return KafkaBroker(**kwargs) - def get_test_broker(self, broker): + def get_test_broker(self, broker) -> TestKafkaBroker: return TestKafkaBroker(broker) diff --git a/tests/asgi/kafka/test_asgi.py b/tests/asgi/kafka/test_asgi.py index cb26b402dc..c180e57c35 100644 --- a/tests/asgi/kafka/test_asgi.py +++ b/tests/asgi/kafka/test_asgi.py @@ -1,10 +1,12 @@ +from typing import Any + from faststream.kafka import KafkaBroker, TestKafkaBroker from tests.asgi.testcase import AsgiTestcase class TestKafkaAsgi(AsgiTestcase): - def get_broker(self): - return KafkaBroker() + def get_broker(self, **kwargs: Any) -> KafkaBroker: + return KafkaBroker(**kwargs) - def get_test_broker(self, broker): + def get_test_broker(self, broker: KafkaBroker) -> TestKafkaBroker: return TestKafkaBroker(broker) diff --git a/tests/asgi/nats/test_asgi.py b/tests/asgi/nats/test_asgi.py index f54f52b25a..2388c9dfea 100644 --- a/tests/asgi/nats/test_asgi.py +++ b/tests/asgi/nats/test_asgi.py @@ -1,10 +1,12 @@ +from typing import Any + from faststream.nats import NatsBroker, TestNatsBroker from tests.asgi.testcase import AsgiTestcase class TestNatsAsgi(AsgiTestcase): - def get_broker(self): - return NatsBroker() + def get_broker(self, **kwargs: Any) -> NatsBroker: + return NatsBroker(**kwargs) - def get_test_broker(self, broker): + def get_test_broker(self, broker: NatsBroker) -> TestNatsBroker: return TestNatsBroker(broker) diff --git a/tests/asgi/rabbit/test_asgi.py b/tests/asgi/rabbit/test_asgi.py index 9df4794225..11c6580f1f 100644 --- a/tests/asgi/rabbit/test_asgi.py +++ b/tests/asgi/rabbit/test_asgi.py @@ -1,10 +1,12 @@ +from typing import Any + from faststream.rabbit import RabbitBroker, TestRabbitBroker from tests.asgi.testcase import AsgiTestcase class TestRabbitAsgi(AsgiTestcase): - def get_broker(self): - return RabbitBroker() + def get_broker(self, **kwargs: Any) -> RabbitBroker: + return RabbitBroker(**kwargs) - def get_test_broker(self, broker): + def get_test_broker(self, broker: RabbitBroker) -> TestRabbitBroker: return TestRabbitBroker(broker) diff --git a/tests/asgi/redis/test_asgi.py b/tests/asgi/redis/test_asgi.py index 3b3e5a38be..e1ee6b28e0 100644 --- a/tests/asgi/redis/test_asgi.py +++ b/tests/asgi/redis/test_asgi.py @@ -1,10 +1,12 @@ +from typing import Any + from faststream.redis import RedisBroker, TestRedisBroker from tests.asgi.testcase import AsgiTestcase class TestRedisAsgi(AsgiTestcase): - def get_broker(self): - return RedisBroker() + def get_broker(self, **kwargs: Any) -> RedisBroker: + return RedisBroker(**kwargs) - def get_test_broker(self, broker): + def get_test_broker(self, broker: RedisBroker) -> TestRedisBroker: return TestRedisBroker(broker) diff --git a/tests/asgi/testcase.py b/tests/asgi/testcase.py index 0407e9d77f..9564ffe667 100644 --- a/tests/asgi/testcase.py +++ b/tests/asgi/testcase.py @@ -5,18 +5,26 @@ from starlette.testclient import TestClient from starlette.websockets import WebSocketDisconnect -from faststream.asgi import AsgiFastStream, AsgiResponse, get, make_ping_asgi +from faststream.asgi import ( + AsgiFastStream, + AsgiResponse, + get, + make_asyncapi_asgi, + make_ping_asgi, +) +from faststream.asgi.types import Scope +from faststream.specification import AsyncAPI class AsgiTestcase: def get_broker(self) -> Any: - raise NotImplementedError() + raise NotImplementedError - def get_test_broker(self, broker) -> Any: - raise NotImplementedError() + def get_test_broker(self, broker: Any) -> Any: + raise NotImplementedError - @pytest.mark.asyncio - async def test_not_found(self): + @pytest.mark.asyncio() + async def test_not_found(self) -> None: broker = self.get_broker() app = AsgiFastStream(broker) @@ -25,8 +33,8 @@ async def test_not_found(self): response = client.get("/") assert response.status_code == 404 - @pytest.mark.asyncio - async def test_ws_not_found(self): + @pytest.mark.asyncio() + async def test_ws_not_found(self) -> None: broker = self.get_broker() app = AsgiFastStream(broker) @@ -37,8 +45,22 @@ async def test_ws_not_found(self): with client.websocket_connect("/ws"): # raises error pass - @pytest.mark.asyncio - async def test_asgi_ping_unhealthy(self): + @pytest.mark.asyncio() + async def test_asgi_ping_healthy(self) -> None: + broker = self.get_broker() + + app = AsgiFastStream( + broker, + asgi_routes=[("/health", make_ping_asgi(broker, timeout=5.0))], + ) + + async with self.get_test_broker(broker): + with TestClient(app) as client: + response = client.get("/health") + assert response.status_code == 204 + + @pytest.mark.asyncio() + async def test_asgi_ping_unhealthy(self) -> None: broker = self.get_broker() app = AsgiFastStream( @@ -55,36 +77,25 @@ async def test_asgi_ping_unhealthy(self): response = client.get("/health") assert response.status_code == 500 - @pytest.mark.asyncio - async def test_asgi_ping_healthy(self): + @pytest.mark.asyncio() + async def test_asyncapi_asgi(self) -> None: broker = self.get_broker() app = AsgiFastStream( broker, - asgi_routes=[("/health", make_ping_asgi(broker, timeout=5.0))], + asgi_routes=[("/docs", make_asyncapi_asgi(AsyncAPI(broker)))], ) - async with self.get_test_broker(broker): - with TestClient(app) as client: - response = client.get("/health") - assert response.status_code == 204 - - @pytest.mark.asyncio - async def test_asyncapi_asgi(self): - broker = self.get_broker() - - app = AsgiFastStream(broker, asyncapi_path="/docs") - async with self.get_test_broker(broker): with TestClient(app) as client: response = client.get("/docs") assert response.status_code == 200 assert response.text - @pytest.mark.asyncio - async def test_get_decorator(self): + @pytest.mark.asyncio() + async def test_get_decorator(self) -> None: @get - async def some_handler(scope): + async def some_handler(scope: Scope) -> AsgiResponse: return AsgiResponse(body=b"test", status_code=200) broker = self.get_broker() diff --git a/tests/asyncapi/base/arguments.py b/tests/asyncapi/base/arguments.py deleted file mode 100644 index f46790f1b3..0000000000 --- a/tests/asyncapi/base/arguments.py +++ /dev/null @@ -1,746 +0,0 @@ -from dataclasses import dataclass, field -from enum import Enum -from typing import List, Optional, Type, Union - -import pydantic -from dirty_equals import IsDict, IsPartialDict, IsStr -from fast_depends import Depends -from fastapi import Depends as APIDepends -from typing_extensions import Annotated, Literal - -from faststream import Context, FastStream -from faststream._compat import PYDANTIC_V2 -from faststream.asyncapi.generate import get_app_schema -from faststream.broker.core.usecase import BrokerUsecase -from tests.marks import pydantic_v2 - - -class FastAPICompatible: - is_fastapi: bool = False - - broker_class: Type[BrokerUsecase] - dependency_builder = staticmethod(APIDepends) - - def build_app(self, broker): - """Patch it to test FastAPI scheme generation too.""" - return FastStream(broker) - - def test_custom_naming(self): - broker = self.broker_class() - - @broker.subscriber("test", title="custom_name", description="test description") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert key == "custom_name" - assert schema["channels"][key]["description"] == "test description" - - def test_docstring_description(self): - broker = self.broker_class() - - @broker.subscriber("test", title="custom_name") - async def handle(msg): - """Test description.""" - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert key == "custom_name" - assert schema["channels"][key]["description"] == "Test description.", schema[ - "channels" - ][key]["description"] - - def test_empty(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "EmptyPayload" - assert v == { - "title": key, - "type": "null", - } - - def test_no_type(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == {"title": key} - - def test_simple_type(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(msg: int): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - assert next(iter(schema["channels"].values())).get("description") is None - - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == {"title": key, "type": "integer"} - - def test_simple_optional_type(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(msg: Optional[int]): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": key, - } - ) | IsDict( - { # TODO: remove when deprecating PydanticV1 - "title": key, - "type": "integer", - } - ), v - - def test_simple_type_with_default(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(msg: int = 1): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == { - "default": 1, - "title": key, - "type": "integer", - } - - def test_multi_args_no_type(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(msg, another): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == { - "properties": { - "another": {"title": "Another"}, - "msg": {"title": "Msg"}, - }, - "required": ["msg", "another"], - "title": key, - "type": "object", - } - - def test_multi_args_with_type(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(msg: str, another: int): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == { - "properties": { - "another": {"title": "Another", "type": "integer"}, - "msg": {"title": "Msg", "type": "string"}, - }, - "required": ["msg", "another"], - "title": key, - "type": "object", - } - - def test_multi_args_with_default(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(msg: str, another: Optional[int] = None): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - - assert v == { - "properties": { - "another": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "default": None, - "title": "Another", - } - ) - | IsDict( - { # TODO: remove when deprecating PydanticV1 - "title": "Another", - "type": "integer", - } - ), - "msg": {"title": "Msg", "type": "string"}, - }, - "required": ["msg"], - "title": key, - "type": "object", - } - - def test_dataclass(self): - @dataclass - class User: - id: int - name: str = "" - - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(user: User): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "User" - assert v == { - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"default": "", "title": "Name", "type": "string"}, - }, - "required": ["id"], - "title": key, - "type": "object", - } - - def test_dataclasses_nested(self): - @dataclass - class Product: - id: int - name: str = "" - - @dataclass - class Order: - id: int - products: List[Product] = field(default_factory=list) - - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(order: Order): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - assert payload == { - "Product": { - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"default": "", "title": "Name", "type": "string"}, - }, - "required": ["id"], - "title": "Product", - "type": "object", - }, - "Order": { - "properties": { - "id": {"title": "Id", "type": "integer"}, - "products": { - "items": {"$ref": "#/components/schemas/Product"}, - "title": "Products", - "type": "array", - }, - }, - "required": ["id"], - "title": "Order", - "type": "object", - }, - } - - def test_pydantic_model(self): - class User(pydantic.BaseModel): - name: str = "" - id: int - - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(user: User): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "User" - assert v == { - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"default": "", "title": "Name", "type": "string"}, - }, - "required": ["id"], - "title": key, - "type": "object", - } - - def test_pydantic_model_with_enum(self): - class Status(str, Enum): - registered = "registered" - banned = "banned" - - class User(pydantic.BaseModel): - name: str = "" - id: int - status: Status - - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(user: User): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - assert payload == { - "Status": IsPartialDict( - { - "enum": ["registered", "banned"], - "title": "Status", - "type": "string", - } - ), - "User": { - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"default": "", "title": "Name", "type": "string"}, - "status": {"$ref": "#/components/schemas/Status"}, - }, - "required": ["id", "status"], - "title": "User", - "type": "object", - }, - }, payload - - def test_pydantic_model_mixed_regular(self): - class Email(pydantic.BaseModel): - addr: str - - class User(pydantic.BaseModel): - name: str = "" - id: int - email: Email - - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(user: User, description: str = ""): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - assert payload == { - "Email": { - "title": "Email", - "type": "object", - "properties": {"addr": {"title": "Addr", "type": "string"}}, - "required": ["addr"], - }, - "User": { - "title": "User", - "type": "object", - "properties": { - "name": {"title": "Name", "default": "", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "email": {"$ref": "#/components/schemas/Email"}, - }, - "required": ["id", "email"], - }, - "Handle:Message:Payload": { - "title": "Handle:Message:Payload", - "type": "object", - "properties": { - "user": {"$ref": "#/components/schemas/User"}, - "description": { - "title": "Description", - "default": "", - "type": "string", - }, - }, - "required": ["user"], - }, - } - - def test_pydantic_model_with_example(self): - class User(pydantic.BaseModel): - name: str = "" - id: int - - if PYDANTIC_V2: - model_config = { - "json_schema_extra": {"examples": [{"name": "john", "id": 1}]} - } - - else: - - class Config: - schema_extra = {"examples": [{"name": "john", "id": 1}]} # noqa: RUF012 - - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(user: User): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "User" - assert v == { - "examples": [{"id": 1, "name": "john"}], - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"default": "", "title": "Name", "type": "string"}, - }, - "required": ["id"], - "title": "User", - "type": "object", - } - - def test_pydantic_model_with_keyword_property(self): - class TestModel(pydantic.BaseModel): - discriminator: int = 0 - - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(model: TestModel): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "TestModel" - assert v == { - "properties": { - "discriminator": { - "default": 0, - "title": "Discriminator", - "type": "integer", - }, - }, - "title": key, - "type": "object", - } - - def test_ignores_depends(self): - broker = self.broker_class() - - def dep(name: str = ""): - return name - - def dep2(name2: str): - return name2 - - dependencies = (self.dependency_builder(dep2),) - message = self.dependency_builder(dep) - - @broker.subscriber("test", dependencies=dependencies) - async def handle(id: int, message=message): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == { - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"default": "", "title": "Name", "type": "string"}, - "name2": {"title": "Name2", "type": "string"}, - }, - "required": ["id", "name2"], - "title": key, - "type": "object", - }, v - - @pydantic_v2 - def test_descriminator(self): - class Sub2(pydantic.BaseModel): - type: Literal["sub2"] - - class Sub(pydantic.BaseModel): - type: Literal["sub"] - - broker = self.broker_class() - - @broker.subscriber("test") - async def handle( - user: Annotated[Union[Sub2, Sub], pydantic.Field(discriminator="type")], - ): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - key = next(iter(schema["components"]["messages"].keys())) - - assert key == IsStr(regex=r"test[\w:]*:Handle:Message") - - expected_schema = IsPartialDict( - { - "discriminator": "type", - "oneOf": [ - {"$ref": "#/components/schemas/Sub2"}, - {"$ref": "#/components/schemas/Sub"}, - ], - "title": "Handle:Message:Payload", - } - ) - if self.is_fastapi: - expected_schema = ( - IsPartialDict( - { - "$ref": "#/components/schemas/Handle:Message:Payload", - } - ) - | expected_schema - ) - - assert schema["components"]["messages"][key]["payload"] == expected_schema, ( - schema["components"] - ) - - assert schema["components"]["schemas"] == IsPartialDict( - { - "Sub": { - "properties": { - "type": IsPartialDict({"const": "sub", "title": "Type"}) - }, - "required": ["type"], - "title": "Sub", - "type": "object", - }, - "Sub2": { - "properties": { - "type": IsPartialDict({"const": "sub2", "title": "Type"}) - }, - "required": ["type"], - "title": "Sub2", - "type": "object", - }, - } - ), schema["components"]["schemas"] - - if self.is_fastapi and ( - payload := schema["components"]["schemas"].get("Handle:Message:Payload") - ): - assert payload == IsPartialDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/Sub2"}, - {"$ref": "#/components/schemas/Sub"}, - ] - } - ) - - @pydantic_v2 - def test_nested_descriminator(self): - class Sub2(pydantic.BaseModel): - type: Literal["sub2"] - - class Sub(pydantic.BaseModel): - type: Literal["sub"] - - class Model(pydantic.BaseModel): - msg: Union[Sub2, Sub] = pydantic.Field(..., discriminator="type") - - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(user: Model): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - key = next(iter(schema["components"]["messages"].keys())) - assert key == IsStr(regex=r"test[\w:]*:Handle:Message") - assert schema["components"] == { - "messages": { - key: IsPartialDict( - { - "payload": {"$ref": "#/components/schemas/Model"}, - } - ) - }, - "schemas": { - "Sub": { - "properties": { - "type": IsPartialDict({"const": "sub", "title": "Type"}) - }, - "required": ["type"], - "title": "Sub", - "type": "object", - }, - "Sub2": { - "properties": { - "type": IsPartialDict({"const": "sub2", "title": "Type"}) - }, - "required": ["type"], - "title": "Sub2", - "type": "object", - }, - "Model": { - "properties": { - "msg": { - "discriminator": "type", - "oneOf": [ - {"$ref": "#/components/schemas/Sub2"}, - {"$ref": "#/components/schemas/Sub"}, - ], - "title": "Msg", - } - }, - "required": ["msg"], - "title": "Model", - "type": "object", - }, - }, - }, schema["components"] - - def test_with_filter(self): - class User(pydantic.BaseModel): - name: str = "" - id: int - - broker = self.broker_class() - - sub = broker.subscriber("test/one") - - @sub( - filter=lambda m: m.content_type == "application/json", - ) - async def handle(id: int): ... - - @sub - async def handle_default(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - name, message = next(iter(schema["components"]["messages"].items())) - - assert name == IsStr( - regex=r"test.one[\w:]*:\[Handle,HandleDefault\]:Message" - ), name - - assert len(message["payload"]["oneOf"]) == 2 - - payload = schema["components"]["schemas"] - - assert "Handle:Message:Payload" in list(payload.keys()) - assert "HandleDefault:Message:Payload" in list(payload.keys()) - - -class ArgumentsTestcase(FastAPICompatible): - dependency_builder = staticmethod(Depends) - - def test_pydantic_field(self): - broker = self.broker_class() - - @broker.subscriber("msg") - async def msg( - msg: pydantic.PositiveInt = pydantic.Field( - 1, - description="some field", - title="Perfect", - examples=[1], - ), - ): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert key == "Perfect" - - assert v == { - "default": 1, - "description": "some field", - "examples": [1], - "exclusiveMinimum": 0, - "title": "Perfect", - "type": "integer", - } - - def test_ignores_custom_field(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(id: int, user: Optional[str] = None, message=Context()): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert v == IsDict( - { - "properties": { - "id": {"title": "Id", "type": "integer"}, - "user": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "default": None, - "title": "User", - }, - }, - "required": ["id"], - "title": key, - "type": "object", - } - ) | IsDict( # TODO: remove when deprecating PydanticV1 - { - "properties": { - "id": {"title": "Id", "type": "integer"}, - "user": {"title": "User", "type": "string"}, - }, - "required": ["id"], - "title": "Handle:Message:Payload", - "type": "object", - } - ) diff --git a/tests/asyncapi/base/fastapi.py b/tests/asyncapi/base/fastapi.py deleted file mode 100644 index 561fd6b232..0000000000 --- a/tests/asyncapi/base/fastapi.py +++ /dev/null @@ -1,130 +0,0 @@ -from typing import Any, Callable, Type - -import pytest -from dirty_equals import IsStr -from fastapi import FastAPI -from fastapi.testclient import TestClient - -from faststream.asyncapi.generate import get_app_schema -from faststream.broker.core.usecase import BrokerUsecase -from faststream.broker.fastapi.router import StreamRouter -from faststream.broker.types import MsgType - - -class FastAPITestCase: - is_fastapi = True - - broker_class: Type[StreamRouter[MsgType]] - broker_wrapper: Callable[[BrokerUsecase[MsgType, Any]], BrokerUsecase[MsgType, Any]] - - @pytest.mark.asyncio - async def test_fastapi_full_information(self): - broker = self.broker_class( - protocol="custom", - protocol_version="1.1.1", - description="Test broker description", - schema_url="/asyncapi_schema", - asyncapi_tags=[{"name": "test"}], - ) - - app = FastAPI( - title="CustomApp", - version="1.1.1", - description="Test description", - contact={"name": "support", "url": "https://support.com"}, - license_info={"name": "some", "url": "https://some.com"}, - ) - - app.include_router(broker) - - async with self.broker_wrapper(broker.broker): - with TestClient(app) as client: - response_json = client.get("/asyncapi_schema.json") - - assert response_json.json() == { - "asyncapi": "2.6.0", - "defaultContentType": "application/json", - "info": { - "title": "CustomApp", - "version": "1.1.1", - "description": "Test description", - "contact": { - "name": "support", - "url": IsStr(regex=r"https\:\/\/support\.com\/?"), - }, - "license": { - "name": "some", - "url": IsStr(regex=r"https\:\/\/some\.com\/?"), - }, - }, - "servers": { - "development": { - "url": IsStr(), - "protocol": "custom", - "description": "Test broker description", - "protocolVersion": "1.1.1", - "tags": [{"name": "test"}], - } - }, - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - } - - @pytest.mark.asyncio - async def test_fastapi_asyncapi_routes(self): - broker = self.broker_class(schema_url="/asyncapi_schema") - - @broker.subscriber("test") - async def handler(): ... - - app = FastAPI() - app.include_router(broker) - - async with self.broker_wrapper(broker.broker): - with TestClient(app) as client: - schema = get_app_schema(broker) - - response_json = client.get("/asyncapi_schema.json") - assert response_json.json() == schema.to_jsonable() - - response_yaml = client.get("/asyncapi_schema.yaml") - assert response_yaml.text == schema.to_yaml() - - response_html = client.get("/asyncapi_schema") - assert response_html.status_code == 200 - - @pytest.mark.asyncio - async def test_fastapi_asyncapi_not_fount(self): - broker = self.broker_class(include_in_schema=False) - - app = FastAPI() - app.include_router(broker) - - async with self.broker_wrapper(broker.broker): - with TestClient(app) as client: - response_json = client.get("/asyncapi.json") - assert response_json.status_code == 404 - - response_yaml = client.get("/asyncapi.yaml") - assert response_yaml.status_code == 404 - - response_html = client.get("/asyncapi") - assert response_html.status_code == 404 - - @pytest.mark.asyncio - async def test_fastapi_asyncapi_not_fount_by_url(self): - broker = self.broker_class(schema_url=None) - - app = FastAPI() - app.include_router(broker) - - async with self.broker_wrapper(broker.broker): - with TestClient(app) as client: - response_json = client.get("/asyncapi.json") - assert response_json.status_code == 404 - - response_yaml = client.get("/asyncapi.yaml") - assert response_yaml.status_code == 404 - - response_html = client.get("/asyncapi") - assert response_html.status_code == 404 diff --git a/tests/asyncapi/base/naming.py b/tests/asyncapi/base/naming.py deleted file mode 100644 index 3a0bff3ad3..0000000000 --- a/tests/asyncapi/base/naming.py +++ /dev/null @@ -1,398 +0,0 @@ -from typing import Any, Type - -from dirty_equals import Contains, IsStr -from pydantic import create_model - -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.broker.core.usecase import BrokerUsecase - - -class BaseNaming: - broker_class: Type[BrokerUsecase[Any, Any]] - - -class SubscriberNaming(BaseNaming): - def test_subscriber_naming(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle_user_created(msg: str): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == [ - IsStr(regex=r"test[\w:]*:HandleUserCreated") - ] - - assert list(schema["components"]["messages"].keys()) == [ - IsStr(regex=r"test[\w:]*:HandleUserCreated:Message") - ] - - assert list(schema["components"]["schemas"].keys()) == [ - "HandleUserCreated:Message:Payload" - ] - - def test_pydantic_subscriber_naming(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle_user_created(msg: create_model("SimpleModel")): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == [ - IsStr(regex=r"test[\w:]*:HandleUserCreated") - ] - - assert list(schema["components"]["messages"].keys()) == [ - IsStr(regex=r"test[\w:]*:HandleUserCreated:Message") - ] - - assert list(schema["components"]["schemas"].keys()) == ["SimpleModel"] - - def test_multi_subscribers_naming(self): - broker = self.broker_class() - - @broker.subscriber("test") - @broker.subscriber("test2") - async def handle_user_created(msg: str): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == [ - IsStr(regex=r"test[\w:]*:HandleUserCreated"), - IsStr(regex=r"test2[\w:]*:HandleUserCreated"), - ] - - assert list(schema["components"]["messages"].keys()) == [ - IsStr(regex=r"test[\w:]*:HandleUserCreated:Message"), - IsStr(regex=r"test2[\w:]*:HandleUserCreated:Message"), - ] - - assert list(schema["components"]["schemas"].keys()) == [ - "HandleUserCreated:Message:Payload" - ] - - def test_subscriber_naming_manual(self): - broker = self.broker_class() - - @broker.subscriber("test", title="custom") - async def handle_user_created(msg: str): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == ["custom"] - - assert list(schema["components"]["messages"].keys()) == ["custom:Message"] - - assert list(schema["components"]["schemas"].keys()) == [ - "custom:Message:Payload" - ] - - def test_subscriber_naming_default(self): - broker = self.broker_class() - - broker.subscriber("test") - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == [ - IsStr(regex=r"test[\w:]*:Subscriber") - ] - - assert list(schema["components"]["messages"].keys()) == [ - IsStr(regex=r"test[\w:]*:Subscriber:Message") - ] - - for key, v in schema["components"]["schemas"].items(): - assert key == "Subscriber:Message:Payload" - assert v == {"title": key} - - def test_subscriber_naming_default_with_title(self): - broker = self.broker_class() - - broker.subscriber("test", title="custom") - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == ["custom"] - - assert list(schema["components"]["messages"].keys()) == ["custom:Message"] - - assert list(schema["components"]["schemas"].keys()) == [ - "custom:Message:Payload" - ] - - assert schema["components"]["schemas"]["custom:Message:Payload"] == { - "title": "custom:Message:Payload" - } - - def test_multi_subscribers_naming_default(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle_user_created(msg: str): ... - - broker.subscriber("test2") - broker.subscriber("test3") - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == [ - IsStr(regex=r"test[\w:]*:HandleUserCreated"), - IsStr(regex=r"test2[\w:]*:Subscriber"), - IsStr(regex=r"test3[\w:]*:Subscriber"), - ] - - assert list(schema["components"]["messages"].keys()) == [ - IsStr(regex=r"test[\w:]*:HandleUserCreated:Message"), - IsStr(regex=r"test2[\w:]*:Subscriber:Message"), - IsStr(regex=r"test3[\w:]*:Subscriber:Message"), - ] - - assert list(schema["components"]["schemas"].keys()) == [ - "HandleUserCreated:Message:Payload", - "Subscriber:Message:Payload", - ] - - assert schema["components"]["schemas"]["Subscriber:Message:Payload"] == { - "title": "Subscriber:Message:Payload" - } - - -class FilterNaming(BaseNaming): - def test_subscriber_filter_base(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle_user_created(msg: str): ... - - @broker.subscriber("test") - async def handle_user_id(msg: int): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == [ - IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]") - ] - - assert list(schema["components"]["messages"].keys()) == [ - IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]:Message") - ] - - assert list(schema["components"]["schemas"].keys()) == [ - "HandleUserCreated:Message:Payload", - "HandleUserId:Message:Payload", - ] - - def test_subscriber_filter_pydantic(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle_user_created(msg: create_model("SimpleModel")): ... - - @broker.subscriber("test") - async def handle_user_id(msg: int): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == [ - IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]") - ] - - assert list(schema["components"]["messages"].keys()) == [ - IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]:Message") - ] - - assert list(schema["components"]["schemas"].keys()) == [ - "SimpleModel", - "HandleUserId:Message:Payload", - ] - - def test_subscriber_filter_with_title(self): - broker = self.broker_class() - - @broker.subscriber("test", title="custom") - async def handle_user_created(msg: str): ... - - @broker.subscriber("test", title="custom") - async def handle_user_id(msg: int): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == ["custom"] - - assert list(schema["components"]["messages"].keys()) == ["custom:Message"] - - assert list(schema["components"]["schemas"].keys()) == [ - "HandleUserCreated:Message:Payload", - "HandleUserId:Message:Payload", - ] - - -class PublisherNaming(BaseNaming): - def test_publisher_naming_base(self): - broker = self.broker_class() - - @broker.publisher("test") - async def handle_user_created() -> str: ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == [IsStr(regex=r"test[\w:]*:Publisher")] - - assert list(schema["components"]["messages"].keys()) == [ - IsStr(regex=r"test[\w:]*:Publisher:Message") - ] - - assert list(schema["components"]["schemas"].keys()) == [ - IsStr(regex=r"test[\w:]*:Publisher:Message:Payload") - ] - - def test_publisher_naming_pydantic(self): - broker = self.broker_class() - - @broker.publisher("test") - async def handle_user_created() -> create_model("SimpleModel"): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == [IsStr(regex=r"test[\w:]*:Publisher")] - - assert list(schema["components"]["messages"].keys()) == [ - IsStr(regex=r"test[\w:]*:Publisher:Message") - ] - - assert list(schema["components"]["schemas"].keys()) == [ - "SimpleModel", - ] - - def test_publisher_manual_naming(self): - broker = self.broker_class() - - @broker.publisher("test", title="custom") - async def handle_user_created() -> str: ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == ["custom"] - - assert list(schema["components"]["messages"].keys()) == ["custom:Message"] - - assert list(schema["components"]["schemas"].keys()) == [ - "custom:Message:Payload" - ] - - def test_publisher_with_schema_naming(self): - broker = self.broker_class() - - @broker.publisher("test", schema=str) - async def handle_user_created(): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == [IsStr(regex=r"test[\w:]*:Publisher")] - - assert list(schema["components"]["messages"].keys()) == [ - IsStr(regex=r"test[\w:]*:Publisher:Message") - ] - - assert list(schema["components"]["schemas"].keys()) == [ - IsStr(regex=r"test[\w:]*:Publisher:Message:Payload") - ] - - def test_publisher_manual_naming_with_schema(self): - broker = self.broker_class() - - @broker.publisher("test", title="custom", schema=str) - async def handle_user_created(): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == ["custom"] - - assert list(schema["components"]["messages"].keys()) == ["custom:Message"] - - assert list(schema["components"]["schemas"].keys()) == [ - "custom:Message:Payload" - ] - - def test_multi_publishers_naming(self): - broker = self.broker_class() - - @broker.publisher("test") - @broker.publisher("test2") - async def handle_user_created() -> str: ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - names = list(schema["channels"].keys()) - assert names == Contains( - IsStr(regex=r"test2[\w:]*:Publisher"), - IsStr(regex=r"test[\w:]*:Publisher"), - ), names - - messages = list(schema["components"]["messages"].keys()) - assert messages == Contains( - IsStr(regex=r"test2[\w:]*:Publisher:Message"), - IsStr(regex=r"test[\w:]*:Publisher:Message"), - ), messages - - payloads = list(schema["components"]["schemas"].keys()) - assert payloads == Contains( - IsStr(regex=r"test2[\w:]*:Publisher:Message:Payload"), - IsStr(regex=r"test[\w:]*:Publisher:Message:Payload"), - ), payloads - - def test_multi_publisher_usages(self): - broker = self.broker_class() - - pub = broker.publisher("test") - - @pub - async def handle_user_created() -> str: ... - - @pub - async def handle() -> int: ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == [ - IsStr(regex=r"test[\w:]*:Publisher"), - ] - - assert list(schema["components"]["messages"].keys()) == [ - IsStr(regex=r"test[\w:]*:Publisher:Message"), - ] - - assert list(schema["components"]["schemas"].keys()) == [ - "HandleUserCreated:Publisher:Message:Payload", - "Handle:Publisher:Message:Payload", - ] - - def test_multi_publisher_usages_with_custom(self): - broker = self.broker_class() - - pub = broker.publisher("test", title="custom") - - @pub - async def handle_user_created() -> str: ... - - @pub - async def handle() -> int: ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == ["custom"] - - assert list(schema["components"]["messages"].keys()) == ["custom:Message"] - - assert list(schema["components"]["schemas"].keys()) == [ - "HandleUserCreated:Publisher:Message:Payload", - "Handle:Publisher:Message:Payload", - ] - - -class NamingTestCase(SubscriberNaming, FilterNaming, PublisherNaming): - pass diff --git a/tests/asyncapi/base/publisher.py b/tests/asyncapi/base/publisher.py deleted file mode 100644 index 00b574e817..0000000000 --- a/tests/asyncapi/base/publisher.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import Type - -import pydantic - -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.broker.core.usecase import BrokerUsecase - - -class PublisherTestcase: - broker_class: Type[BrokerUsecase] - - def build_app(self, broker): - """Patch it to test FastAPI scheme generation too.""" - return FastStream(broker) - - def test_publisher_with_description(self): - broker = self.broker_class() - - @broker.publisher("test", description="test description") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - assert schema["channels"][key]["description"] == "test description" - - def test_basic_publisher(self): - broker = self.broker_class() - - @broker.publisher("test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - assert schema["channels"][key].get("description") is None - assert schema["channels"][key].get("publish") is not None - - payload = schema["components"]["schemas"] - for v in payload.values(): - assert v == {} - - def test_none_publisher(self): - broker = self.broker_class() - - @broker.publisher("test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - for v in payload.values(): - assert v == {} - - def test_typed_publisher(self): - broker = self.broker_class() - - @broker.publisher("test") - async def handle(msg) -> int: ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - for v in payload.values(): - assert v["type"] == "integer" - - def test_pydantic_model_publisher(self): - class User(pydantic.BaseModel): - name: str = "" - id: int - - broker = self.broker_class() - - @broker.publisher("test") - async def handle(msg) -> User: ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert v == { - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"default": "", "title": "Name", "type": "string"}, - }, - "required": ["id"], - "title": key, - "type": "object", - } - - def test_delayed(self): - broker = self.broker_class() - - pub = broker.publisher("test") - - @pub - async def handle(msg) -> int: ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - for v in payload.values(): - assert v["type"] == "integer" - - def test_with_schema(self): - broker = self.broker_class() - - broker.publisher("test", title="Custom", schema=int) - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - for v in payload.values(): - assert v["type"] == "integer" - - def test_not_include(self): - broker = self.broker_class() - - @broker.publisher("test", include_in_schema=False) - @broker.subscriber("in-test", include_in_schema=False) - async def handler(msg: str): - pass - - schema = get_app_schema(self.build_app(broker)) - - assert schema.channels == {}, schema.channels - - def test_pydantic_model_with_keyword_property_publisher(self): - class TestModel(pydantic.BaseModel): - discriminator: int = 0 - - broker = self.broker_class() - - @broker.publisher("test") - async def handle(msg) -> TestModel: ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - - for key, v in payload.items(): - assert v == { - "properties": { - "discriminator": { - "default": 0, - "title": "Discriminator", - "type": "integer", - }, - }, - "title": key, - "type": "object", - } diff --git a/tests/asyncapi/base/router.py b/tests/asyncapi/base/router.py deleted file mode 100644 index 84996ccb06..0000000000 --- a/tests/asyncapi/base/router.py +++ /dev/null @@ -1,165 +0,0 @@ -from typing import Type - -from dirty_equals import IsStr - -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.broker.core.usecase import BrokerUsecase -from faststream.broker.router import ArgsContainer, BrokerRouter, SubscriberRoute - - -class RouterTestcase: - broker_class: Type[BrokerUsecase] - router_class: Type[BrokerRouter] - publisher_class: Type[ArgsContainer] - route_class: Type[SubscriberRoute] - - def test_delay_subscriber(self): - broker = self.broker_class() - - async def handle(msg): ... - - router = self.router_class( - handlers=(self.route_class(handle, "test"),), - ) - - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - payload = schema["components"]["schemas"] - key = list(payload.keys())[0] # noqa: RUF015 - assert payload[key]["title"] == key == "Handle:Message:Payload" - - def test_delay_publisher(self): - broker = self.broker_class() - - async def handle(msg): ... - - router = self.router_class( - handlers=( - self.route_class( - handle, - "test", - publishers=(self.publisher_class("test2", schema=int),), - ), - ), - ) - - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)) - schemas = schema.components.schemas - del schemas["Handle:Message:Payload"] - - for i, j in schemas.items(): - assert ( - i == j["title"] == IsStr(regex=r"test2[\w:]*:Publisher:Message:Payload") - ) - assert j["type"] == "integer" - - def test_not_include(self): - broker = self.broker_class() - router = self.router_class(include_in_schema=False) - - @router.subscriber("test") - @router.publisher("test") - async def handle(msg): ... - - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)) - assert schema.channels == {}, schema.channels - - def test_not_include_in_method(self): - broker = self.broker_class() - router = self.router_class() - - @router.subscriber("test") - @router.publisher("test") - async def handle(msg): ... - - broker.include_router(router, include_in_schema=False) - - schema = get_app_schema(FastStream(broker)) - assert schema.channels == {}, schema.channels - - def test_respect_subrouter(self): - broker = self.broker_class() - router = self.router_class() - router2 = self.router_class(include_in_schema=False) - - @router2.subscriber("test") - @router2.publisher("test") - async def handle(msg): ... - - router.include_router(router2) - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)) - - assert schema.channels == {}, schema.channels - - def test_not_include_subrouter(self): - broker = self.broker_class() - router = self.router_class(include_in_schema=False) - router2 = self.router_class() - - @router2.subscriber("test") - @router2.publisher("test") - async def handle(msg): ... - - router.include_router(router2) - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)) - - assert schema.channels == {} - - def test_not_include_subrouter_by_method(self): - broker = self.broker_class() - router = self.router_class() - router2 = self.router_class() - - @router2.subscriber("test") - @router2.publisher("test") - async def handle(msg): ... - - router.include_router(router2, include_in_schema=False) - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)) - - assert schema.channels == {} - - def test_all_nested_routers_by_method(self): - broker = self.broker_class() - router = self.router_class() - router2 = self.router_class() - - @router2.subscriber("test") - @router2.publisher("test") - async def handle(msg): ... - - router.include_router(router2) - broker.include_router(router, include_in_schema=False) - - schema = get_app_schema(FastStream(broker)) - - assert schema.channels == {} - - def test_include_subrouter(self): - broker = self.broker_class() - router = self.router_class() - router2 = self.router_class() - - @router2.subscriber("test") - @router2.publisher("test") - async def handle(msg): ... - - router.include_router(router2) - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)) - - assert len(schema.channels) == 2 diff --git a/faststream/cli/utils/__init__.py b/tests/asyncapi/base/v2_6_0/__init__.py similarity index 100% rename from faststream/cli/utils/__init__.py rename to tests/asyncapi/base/v2_6_0/__init__.py diff --git a/tests/asyncapi/base/v2_6_0/arguments.py b/tests/asyncapi/base/v2_6_0/arguments.py new file mode 100644 index 0000000000..0b9c2e725a --- /dev/null +++ b/tests/asyncapi/base/v2_6_0/arguments.py @@ -0,0 +1,796 @@ +from dataclasses import dataclass, field +from enum import Enum +from typing import Annotated, Any, Literal + +import pydantic +import pytest +from dirty_equals import IsDict, IsPartialDict, IsStr +from fast_depends import Depends + +from faststream import Context +from faststream._internal._compat import PYDANTIC_V2 +from faststream._internal.broker import BrokerUsecase +from faststream.specification.asyncapi import AsyncAPI +from tests.marks import pydantic_v2 + + +class FastAPICompatible: + is_fastapi: bool = False + + broker_class: type[BrokerUsecase] + dependency_builder = staticmethod(Depends) + + def build_app(self, broker: BrokerUsecase[Any, Any]) -> BrokerUsecase[Any, Any]: + """Patch it to test FastAPI scheme generation too.""" + return broker + + def test_custom_naming(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test", title="custom_name", description="test description") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert key == "custom_name" + assert schema["channels"][key]["description"] == "test description" + + def test_slash_in_title(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test", title="/") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + assert next(iter(schema["channels"].keys())) == "/" + + assert next(iter(schema["components"]["messages"].keys())) == ".:Message" + assert schema["components"]["messages"][".:Message"]["title"] == "/:Message" + + assert next(iter(schema["components"]["schemas"].keys())) == ".:Message:Payload" + assert ( + schema["components"]["schemas"][".:Message:Payload"]["title"] + == "/:Message:Payload" + ) + + def test_docstring_description(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test", title="custom_name") + async def handle(msg) -> None: + """Test description.""" + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert key == "custom_name" + assert schema["channels"][key]["description"] == "Test description.", schema[ + "channels" + ][key] + + def test_empty(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "EmptyPayload" + assert v == { + "title": key, + "type": "null", + } + + def test_no_type(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == {"title": key} + + def test_simple_type(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(msg: int) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + assert next(iter(schema["channels"].values())).get("description") is None + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == {"title": key, "type": "integer"} + + def test_simple_optional_type(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(msg: int | None) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": key, + }, + ) | IsDict( + { # TODO: remove when deprecating PydanticV1 + "title": key, + "type": "integer", + }, + ), v + + def test_simple_type_with_default(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(msg: int = 1) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == { + "default": 1, + "title": key, + "type": "integer", + } + + def test_multi_args_no_type(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(msg, another) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == { + "properties": { + "another": {"title": "Another"}, + "msg": {"title": "Msg"}, + }, + "required": ["msg", "another"], + "title": key, + "type": "object", + } + + def test_multi_args_with_type(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(msg: str, another: int) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == { + "properties": { + "another": {"title": "Another", "type": "integer"}, + "msg": {"title": "Msg", "type": "string"}, + }, + "required": ["msg", "another"], + "title": key, + "type": "object", + } + + def test_multi_args_with_default(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(msg: str, another: int | None = None) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + + assert v == { + "properties": { + "another": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": None, + "title": "Another", + }, + ) + | IsDict( + { # TODO: remove when deprecating PydanticV1 + "title": "Another", + "type": "integer", + }, + ), + "msg": {"title": "Msg", "type": "string"}, + }, + "required": ["msg"], + "title": key, + "type": "object", + } + + def test_dataclass(self) -> None: + @dataclass + class User: + id: int + name: str = "" + + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(user: User) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "User" + assert v == { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + }, + "required": ["id"], + "title": key, + "type": "object", + } + + def test_dataclasses_nested(self): + @dataclass + class Product: + id: int + name: str = "" + + @dataclass + class Order: + id: int + products: list[Product] = field(default_factory=list) + + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(order: Order): ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + assert payload == { + "Product": { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + }, + "required": ["id"], + "title": "Product", + "type": "object", + }, + "Order": { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "products": { + "items": {"$ref": "#/components/schemas/Product"}, + "title": "Products", + "type": "array", + }, + }, + "required": ["id"], + "title": "Order", + "type": "object", + }, + } + + def test_pydantic_model(self): + class User(pydantic.BaseModel): + name: str = "" + id: int + + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(user: User) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "User" + assert v == { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + }, + "required": ["id"], + "title": key, + "type": "object", + } + + def test_pydantic_model_with_enum(self) -> None: + class Status(str, Enum): + registered = "registered" + banned = "banned" + + class User(pydantic.BaseModel): + name: str = "" + id: int + status: Status + + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(user: User) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + assert payload == { + "Status": IsPartialDict( + { + "enum": ["registered", "banned"], + "title": "Status", + "type": "string", + }, + ), + "User": { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + "status": {"$ref": "#/components/schemas/Status"}, + }, + "required": ["id", "status"], + "title": "User", + "type": "object", + }, + }, payload + + def test_pydantic_model_mixed_regular(self) -> None: + class Email(pydantic.BaseModel): + addr: str + + class User(pydantic.BaseModel): + name: str = "" + id: int + email: Email + + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(user: User, description: str = "") -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + assert payload == { + "Email": { + "title": "Email", + "type": "object", + "properties": {"addr": {"title": "Addr", "type": "string"}}, + "required": ["addr"], + }, + "User": { + "title": "User", + "type": "object", + "properties": { + "name": {"title": "Name", "default": "", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + "email": {"$ref": "#/components/schemas/Email"}, + }, + "required": ["id", "email"], + }, + "Handle:Message:Payload": { + "title": "Handle:Message:Payload", + "type": "object", + "properties": { + "user": {"$ref": "#/components/schemas/User"}, + "description": { + "title": "Description", + "default": "", + "type": "string", + }, + }, + "required": ["user"], + }, + } + + def test_pydantic_model_with_example(self) -> None: + class User(pydantic.BaseModel): + name: str = "" + id: int + + if PYDANTIC_V2: + model_config = { + "json_schema_extra": {"examples": [{"name": "john", "id": 1}]}, + } + + else: + + class Config: + schema_extra = {"examples": [{"name": "john", "id": 1}]} # noqa: RUF012 + + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(user: User) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "User" + assert v == { + "examples": [{"id": 1, "name": "john"}], + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + }, + "required": ["id"], + "title": "User", + "type": "object", + } + + def test_pydantic_model_with_keyword_property(self) -> None: + class TestModel(pydantic.BaseModel): + discriminator: int = 0 + + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(model: TestModel) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "TestModel" + assert v == { + "properties": { + "discriminator": { + "default": 0, + "title": "Discriminator", + "type": "integer", + }, + }, + "title": key, + "type": "object", + } + + def test_ignores_depends(self) -> None: + broker = self.broker_class() + + def dep(name: str = "") -> str: + return name + + def dep2(name2: str) -> str: + return name2 + + dependencies = (self.dependency_builder(dep2),) + message = self.dependency_builder(dep) + + @broker.subscriber("test", dependencies=dependencies) + async def handle(id: int, message=message) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + "name2": {"title": "Name2", "type": "string"}, + }, + "required": ["id", "name2"], + "title": key, + "type": "object", + }, v + + @pydantic_v2 + def test_descriminator(self) -> None: + class Sub2(pydantic.BaseModel): + type: Literal["sub2"] + + class Sub(pydantic.BaseModel): + type: Literal["sub"] + + broker = self.broker_class() + + @broker.subscriber("test") + async def handle( + user: Annotated[Sub2 | Sub, pydantic.Field(discriminator="type")], + ): ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + key = next(iter(schema["components"]["messages"].keys())) + + assert key == IsStr(regex=r"test[\w:]*:Handle:Message"), key + + expected_schema = IsPartialDict({ + "discriminator": "type", + "oneOf": [ + {"$ref": "#/components/schemas/Sub2"}, + {"$ref": "#/components/schemas/Sub"}, + ], + "title": "Handle:Message:Payload", + }) + + fastapi_payload = schema["components"]["schemas"].get("Handle:Message:Payload") + if self.is_fastapi: + if fastapi_payload: + assert fastapi_payload == IsPartialDict({ + "anyOf": [ + {"$ref": "#/components/schemas/Sub2"}, + {"$ref": "#/components/schemas/Sub"}, + ] + }) + + expected_schema = ( + IsPartialDict({"$ref": "#/components/schemas/Handle:Message:Payload"}) + | expected_schema + ) + + assert schema["components"]["messages"][key]["payload"] == expected_schema, ( + schema["components"] + ) + + assert schema["components"]["schemas"] == IsPartialDict({ + "Sub": { + "properties": { + "type": IsPartialDict({"const": "sub", "title": "Type"}), + }, + "required": ["type"], + "title": "Sub", + "type": "object", + }, + "Sub2": { + "properties": { + "type": IsPartialDict({"const": "sub2", "title": "Type"}), + }, + "required": ["type"], + "title": "Sub2", + "type": "object", + }, + }), schema["components"]["schemas"] + + @pydantic_v2 + def test_nested_descriminator(self) -> None: + class Sub2(pydantic.BaseModel): + type: Literal["sub2"] + + class Sub(pydantic.BaseModel): + type: Literal["sub"] + + class Model(pydantic.BaseModel): + msg: Sub2 | Sub = pydantic.Field(..., discriminator="type") + + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(user: Model) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + key = next(iter(schema["components"]["messages"].keys())) + assert key == IsStr(regex=r"test[\w:]*:Handle:Message") + assert schema["components"] == { + "messages": { + key: IsPartialDict({ + "payload": {"$ref": "#/components/schemas/Model"}, + }) + }, + "schemas": { + "Sub": { + "properties": { + "type": IsPartialDict({"const": "sub", "title": "Type"}), + }, + "required": ["type"], + "title": "Sub", + "type": "object", + }, + "Sub2": { + "properties": { + "type": IsPartialDict({"const": "sub2", "title": "Type"}), + }, + "required": ["type"], + "title": "Sub2", + "type": "object", + }, + "Model": { + "properties": { + "msg": { + "discriminator": "type", + "oneOf": [ + {"$ref": "#/components/schemas/Sub2"}, + {"$ref": "#/components/schemas/Sub"}, + ], + "title": "Msg", + }, + }, + "required": ["msg"], + "title": "Model", + "type": "object", + }, + }, + }, schema["components"] + + def test_with_filter(self) -> None: + class User(pydantic.BaseModel): + name: str = "" + id: int + + broker = self.broker_class() + + sub = broker.subscriber("test") + + @sub( + filter=lambda m: m.content_type == "application/json", + ) + async def handle(id: int) -> None: ... + + @sub + async def handle_default(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + name, message = next(iter(schema["components"]["messages"].items())) + + assert name == IsStr(regex=r"test[\w:]*:\[Handle,HandleDefault\]:Message"), name + + assert len(message["payload"]["oneOf"]) == 2 + + payload = schema["components"]["schemas"] + + assert "Handle:Message:Payload" in list(payload.keys()) + assert "HandleDefault:Message:Payload" in list(payload.keys()) + + +class ArgumentsTestcase(FastAPICompatible): + dependency_builder = staticmethod(Depends) + + def test_pydantic_field(self) -> None: + broker = self.broker_class() + + @broker.subscriber("msg") + async def msg( + msg: pydantic.PositiveInt = pydantic.Field( + 1, + description="some field", + title="Perfect", + examples=[1], + ), + ) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Perfect" + + assert v == { + "default": 1, + "description": "some field", + "examples": [1], + "exclusiveMinimum": 0, + "title": "Perfect", + "type": "integer", + } + + def test_ignores_custom_field(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle( + id: int, user: str | None = None, message=Context() + ) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert v == IsDict( + { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "user": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "default": None, + "title": "User", + }, + }, + "required": ["id"], + "title": key, + "type": "object", + }, + ) | IsDict( # TODO: remove when deprecating PydanticV1 + { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "user": {"title": "User", "type": "string"}, + }, + "required": ["id"], + "title": "Handle:Message:Payload", + "type": "object", + }, + ) + + def test_overwrite_schema(self) -> None: + @dataclass + class User: + id: int + name: str = "" + + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(user: User) -> None: ... + + @dataclass + class User: + id: int + email: str = "" + + @broker.subscriber("test2") + async def second_handle(user: User) -> None: ... + + with pytest.warns( + RuntimeWarning, + match="Overwriting the message schema, data types have the same name", + ): + schema = AsyncAPI( + self.build_app(broker), schema_version="2.6.0" + ).to_jsonable() + + payload = schema["components"]["schemas"] + + assert len(payload) == 1 + + key, value = next(iter(payload.items())) + + assert key == "User" + assert value == { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "email": {"default": "", "title": "Email", "type": "string"}, + }, + "required": ["id"], + "title": key, + "type": "object", + } diff --git a/tests/asyncapi/base/v2_6_0/fastapi.py b/tests/asyncapi/base/v2_6_0/fastapi.py new file mode 100644 index 0000000000..25bc607aee --- /dev/null +++ b/tests/asyncapi/base/v2_6_0/fastapi.py @@ -0,0 +1,134 @@ +from collections.abc import Callable +from typing import Any + +import pytest +from dirty_equals import IsStr +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +from faststream._internal.broker import BrokerUsecase +from faststream._internal.fastapi.router import StreamRouter +from faststream._internal.types import MsgType +from faststream.specification.asyncapi import AsyncAPI + + +class FastAPITestCase: + is_fastapi = True + + 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: + router = self.router_class( + protocol="custom", + protocol_version="1.1.1", + description="Test broker description", + schema_url="/asyncapi_schema", + specification_tags=[{"name": "test"}], + ) + + app = FastAPI( + title="CustomApp", + version="1.1.1", + description="Test description", + contact={"name": "support", "url": "https://support.com"}, + license_info={"name": "some", "url": "https://some.com"}, + ) + app.include_router(router) + + async with self.broker_wrapper(router.broker): + with TestClient(app) as client: + response_json = client.get("/asyncapi_schema.json") + + assert response_json.json() == { + "asyncapi": "2.6.0", + "defaultContentType": "application/json", + "info": { + "title": "CustomApp", + "version": "1.1.1", + "description": "Test description", + "contact": { + "name": "support", + "url": IsStr(regex=r"https\:\/\/support\.com\/?"), + }, + "license": { + "name": "some", + "url": IsStr(regex=r"https\:\/\/some\.com\/?"), + }, + }, + "servers": { + "development": { + "url": IsStr(), + "protocol": "custom", + "description": "Test broker description", + "protocolVersion": "1.1.1", + "tags": [{"name": "test"}], + }, + }, + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + } + + @pytest.mark.skip() + @pytest.mark.asyncio() + async def test_fastapi_asyncapi_routes(self) -> None: + router = self.router_class(schema_url="/asyncapi_schema") + + @router.subscriber("test") + async def handler() -> None: ... + + app = FastAPI() + app.include_router(router) + + async with self.broker_wrapper(router.broker): + with TestClient(app) as client: + schema = AsyncAPI(router.broker, schema_version="2.6.0") + + response_json = client.get("/asyncapi_schema.json") + assert response_json.json() == schema.to_jsonable() + + response_yaml = client.get("/asyncapi_schema.yaml") + assert response_yaml.text == schema.to_yaml() + + response_html = client.get("/asyncapi_schema") + assert response_html.status_code == 200 + + @pytest.mark.asyncio() + async def test_fastapi_asyncapi_not_fount(self) -> None: + router = self.router_class(include_in_schema=False) + + app = FastAPI() + app.include_router(router) + + async with self.broker_wrapper(router.broker): + with TestClient(app) as client: + response_json = client.get("/asyncapi.json") + assert response_json.status_code == 404 + + response_yaml = client.get("/asyncapi.yaml") + assert response_yaml.status_code == 404 + + response_html = client.get("/asyncapi") + assert response_html.status_code == 404 + + @pytest.mark.asyncio() + async def test_fastapi_asyncapi_not_fount_by_url(self) -> None: + router = self.router_class(schema_url=None) + + app = FastAPI() + app.include_router(router) + + async with self.broker_wrapper(router.broker): + with TestClient(app) as client: + response_json = client.get("/asyncapi.json") + assert response_json.status_code == 404 + + response_yaml = client.get("/asyncapi.yaml") + assert response_yaml.status_code == 404 + + response_html = client.get("/asyncapi") + assert response_html.status_code == 404 diff --git a/tests/a_docs/__init__.py b/tests/asyncapi/base/v2_6_0/from_spec/__init__.py similarity index 100% rename from tests/a_docs/__init__.py rename to tests/asyncapi/base/v2_6_0/from_spec/__init__.py 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/naming.py b/tests/asyncapi/base/v2_6_0/naming.py new file mode 100644 index 0000000000..ee8b6bcdbf --- /dev/null +++ b/tests/asyncapi/base/v2_6_0/naming.py @@ -0,0 +1,403 @@ +from typing import Any + +from dirty_equals import Contains, IsStr +from pydantic import create_model + +from faststream._internal.broker import BrokerUsecase +from faststream.specification.asyncapi import AsyncAPI + + +class BaseNaming: + broker_class: type[BrokerUsecase[Any, Any]] + + +class SubscriberNaming(BaseNaming): + def test_subscriber_naming(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle_user_created(msg: str) -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Message:Payload", + ] + + def test_pydantic_subscriber_naming(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle_user_created(msg: create_model("SimpleModel")) -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == ["SimpleModel"] + + def test_multi_subscribers_naming(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + @broker.subscriber("test2") + async def handle_user_created(msg: str) -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated"), + IsStr(regex=r"test2[\w:]*:HandleUserCreated"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated:Message"), + IsStr(regex=r"test2[\w:]*:HandleUserCreated:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Message:Payload", + ] + + def test_subscriber_naming_manual(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test", title="custom") + async def handle_user_created(msg: str) -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == ["custom:Message"] + + assert list(schema["components"]["schemas"].keys()) == [ + "custom:Message:Payload", + ] + + def test_subscriber_naming_default(self) -> None: + broker = self.broker_class() + + broker.subscriber("test") + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:Subscriber"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:Subscriber:Message"), + ] + + for key, v in schema["components"]["schemas"].items(): + assert key == "Subscriber:Message:Payload" + assert v == {"title": key} + + def test_subscriber_naming_default_with_title(self) -> None: + broker = self.broker_class() + + broker.subscriber("test", title="custom") + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == ["custom:Message"] + + assert list(schema["components"]["schemas"].keys()) == [ + "custom:Message:Payload", + ] + + assert schema["components"]["schemas"]["custom:Message:Payload"] == { + "title": "custom:Message:Payload", + } + + def test_multi_subscribers_naming_default(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle_user_created(msg: str) -> None: ... + + broker.subscriber("test2") + broker.subscriber("test3") + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated"), + IsStr(regex=r"test2[\w:]*:Subscriber"), + IsStr(regex=r"test3[\w:]*:Subscriber"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated:Message"), + IsStr(regex=r"test2[\w:]*:Subscriber:Message"), + IsStr(regex=r"test3[\w:]*:Subscriber:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Message:Payload", + "Subscriber:Message:Payload", + ] + + assert schema["components"]["schemas"]["Subscriber:Message:Payload"] == { + "title": "Subscriber:Message:Payload", + } + + +class FilterNaming(BaseNaming): + def test_subscriber_filter_base(self) -> None: + broker = self.broker_class() + + sub = broker.subscriber("test") + + @sub + async def handle_user_created(msg: str) -> None: ... + + @sub + async def handle_user_id(msg: int) -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]") + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]:Message") + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Message:Payload", + "HandleUserId:Message:Payload", + ] + + def test_subscriber_filter_pydantic(self) -> None: + broker = self.broker_class() + + sub = broker.subscriber("test") + + @sub + async def handle_user_created(msg: create_model("SimpleModel")) -> None: ... + + @sub + async def handle_user_id(msg: int) -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]") + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]:Message") + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "SimpleModel", + "HandleUserId:Message:Payload", + ] + + def test_subscriber_filter_with_title(self) -> None: + broker = self.broker_class() + + sub = broker.subscriber("test", title="custom") + + @sub + async def handle_user_created(msg: str) -> None: ... + + @sub + async def handle_user_id(msg: int) -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == ["custom:Message"] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Message:Payload", + "HandleUserId:Message:Payload", + ] + + +class PublisherNaming(BaseNaming): + def test_publisher_naming_base(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + async def handle_user_created() -> str: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == [IsStr(regex=r"test[\w:]*:Publisher")] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message:Payload"), + ] + + def test_publisher_naming_pydantic(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + async def handle_user_created() -> create_model("SimpleModel"): ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == [IsStr(regex=r"test[\w:]*:Publisher")] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "SimpleModel", + ] + + def test_publisher_manual_naming(self) -> None: + broker = self.broker_class() + + @broker.publisher("test", title="custom") + async def handle_user_created() -> str: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == ["custom:Message"] + + assert list(schema["components"]["schemas"].keys()) == [ + "custom:Message:Payload", + ] + + def test_publisher_with_schema_naming(self) -> None: + broker = self.broker_class() + + @broker.publisher("test", schema=str) + async def handle_user_created() -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == [IsStr(regex=r"test[\w:]*:Publisher")] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message:Payload"), + ] + + def test_publisher_manual_naming_with_schema(self) -> None: + broker = self.broker_class() + + @broker.publisher("test", title="custom", schema=str) + async def handle_user_created() -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == ["custom:Message"] + + assert list(schema["components"]["schemas"].keys()) == [ + "custom:Message:Payload", + ] + + def test_multi_publishers_naming(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + @broker.publisher("test2") + async def handle_user_created() -> str: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + names = list(schema["channels"].keys()) + assert names == Contains( + IsStr(regex=r"test2[\w:]*:Publisher"), + IsStr(regex=r"test[\w:]*:Publisher"), + ), names + + messages = list(schema["components"]["messages"].keys()) + assert messages == Contains( + IsStr(regex=r"test2[\w:]*:Publisher:Message"), + IsStr(regex=r"test[\w:]*:Publisher:Message"), + ), messages + + payloads = list(schema["components"]["schemas"].keys()) + assert payloads == Contains( + IsStr(regex=r"test2[\w:]*:Publisher:Message:Payload"), + IsStr(regex=r"test[\w:]*:Publisher:Message:Payload"), + ), payloads + + def test_multi_publisher_usages(self) -> None: + broker = self.broker_class() + + pub = broker.publisher("test") + + @pub + async def handle_user_created() -> str: ... + + @pub + async def handle() -> int: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Publisher:Message:Payload", + "Handle:Publisher:Message:Payload", + ] + + def test_multi_publisher_usages_with_custom(self) -> None: + broker = self.broker_class() + + pub = broker.publisher("test", title="custom") + + @pub + async def handle_user_created() -> str: ... + + @pub + async def handle() -> int: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == ["custom:Message"] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Publisher:Message:Payload", + "Handle:Publisher:Message:Payload", + ] + + +class NamingTestCase(SubscriberNaming, FilterNaming, PublisherNaming): + pass diff --git a/tests/asyncapi/base/v2_6_0/publisher.py b/tests/asyncapi/base/v2_6_0/publisher.py new file mode 100644 index 0000000000..e8dfb34fa2 --- /dev/null +++ b/tests/asyncapi/base/v2_6_0/publisher.py @@ -0,0 +1,151 @@ +import pydantic + +from faststream._internal.broker import BrokerUsecase +from faststream.specification.asyncapi import AsyncAPI + + +class PublisherTestcase: + broker_class: type[BrokerUsecase] + + def build_app(self, broker): + """Patch it to test FastAPI scheme generation too.""" + return broker + + def test_publisher_with_description(self) -> None: + broker = self.broker_class() + + @broker.publisher("test", description="test description") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + assert schema["channels"][key]["description"] == "test description" + + def test_basic_publisher(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + assert schema["channels"][key].get("description") is None + assert schema["channels"][key].get("subscribe") is not None + + payload = schema["components"]["schemas"] + for v in payload.values(): + assert v == {} + + def test_none_publisher(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + for v in payload.values(): + assert v == {} + + def test_typed_publisher(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + async def handle(msg) -> int: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + for v in payload.values(): + assert v["type"] == "integer" + + def test_pydantic_model_publisher(self) -> None: + class User(pydantic.BaseModel): + name: str = "" + id: int + + broker = self.broker_class() + + @broker.publisher("test") + async def handle(msg) -> User: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert v == { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + }, + "required": ["id"], + "title": key, + "type": "object", + } + + def test_delayed(self) -> None: + broker = self.broker_class() + + pub = broker.publisher("test") + + @pub + async def handle(msg) -> int: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + for v in payload.values(): + assert v["type"] == "integer" + + def test_with_schema(self) -> None: + broker = self.broker_class() + + broker.publisher("test", title="Custom", schema=int) + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + for v in payload.values(): + assert v["type"] == "integer" + + def test_not_include(self) -> None: + broker = self.broker_class() + + @broker.publisher("test", include_in_schema=False) + @broker.subscriber("in-test", include_in_schema=False) + async def handler(msg: str) -> None: + pass + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0") + + assert schema.to_jsonable()["channels"] == {}, schema.to_jsonable()["channels"] + + def test_pydantic_model_with_keyword_property_publisher(self) -> None: + class TestModel(pydantic.BaseModel): + discriminator: int = 0 + + broker = self.broker_class() + + @broker.publisher("test") + async def handle(msg) -> TestModel: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert v == { + "properties": { + "discriminator": { + "default": 0, + "title": "Discriminator", + "type": "integer", + }, + }, + "title": key, + "type": "object", + } diff --git a/tests/asyncapi/base/v2_6_0/router.py b/tests/asyncapi/base/v2_6_0/router.py new file mode 100644 index 0000000000..03af29b46c --- /dev/null +++ b/tests/asyncapi/base/v2_6_0/router.py @@ -0,0 +1,166 @@ +from dirty_equals import IsStr + +from faststream._internal.broker import BrokerUsecase +from faststream._internal.broker.router import ( + ArgsContainer, + BrokerRouter, + SubscriberRoute, +) +from faststream.specification.asyncapi import AsyncAPI + + +class RouterTestcase: + broker_class: type[BrokerUsecase] + router_class: type[BrokerRouter] + publisher_class: type[ArgsContainer] + route_class: type[SubscriberRoute] + + def test_delay_subscriber(self) -> None: + broker = self.broker_class() + + async def handle(msg) -> None: ... + + router = self.router_class( + handlers=(self.route_class(handle, "test"),), + ) + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + payload = schema["components"]["schemas"] + key = list(payload.keys())[0] # noqa: RUF015 + assert payload[key]["title"] == key == "Handle:Message:Payload" + + def test_delay_publisher(self) -> None: + broker = self.broker_class() + + async def handle(msg) -> None: ... + + router = self.router_class( + handlers=( + self.route_class( + handle, + "test", + publishers=(self.publisher_class("test2", schema=int),), + ), + ), + ) + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0") + schemas = schema.to_jsonable()["components"]["schemas"] + del schemas["Handle:Message:Payload"] + + for i, j in schemas.items(): + assert ( + i == j["title"] == IsStr(regex=r"test2[\w:]*:Publisher:Message:Payload") + ) + assert j["type"] == "integer" + + def test_not_include(self) -> None: + broker = self.broker_class() + router = self.router_class(include_in_schema=False) + + @router.subscriber("test") + @router.publisher("test") + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0") + assert schema.to_jsonable()["channels"] == {}, schema.to_jsonable()["channels"] + + def test_not_include_in_method(self) -> None: + broker = self.broker_class() + router = self.router_class() + + @router.subscriber("test") + @router.publisher("test") + async def handle(msg) -> None: ... + + broker.include_router(router, include_in_schema=False) + + schema = AsyncAPI(broker, schema_version="2.6.0") + assert schema.to_jsonable()["channels"] == {}, schema.to_jsonable()["channels"] + + def test_respect_subrouter(self) -> None: + broker = self.broker_class() + router = self.router_class() + router2 = self.router_class(include_in_schema=False) + + @router2.subscriber("test") + @router2.publisher("test") + async def handle(msg) -> None: ... + + router.include_router(router2) + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0") + + assert schema.to_jsonable()["channels"] == {}, schema.to_jsonable()["channels"] + + def test_not_include_subrouter(self) -> None: + broker = self.broker_class() + router = self.router_class(include_in_schema=False) + router2 = self.router_class() + + @router2.subscriber("test") + @router2.publisher("test") + async def handle(msg) -> None: ... + + router.include_router(router2) + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0") + + assert schema.to_jsonable()["channels"] == {} + + def test_not_include_subrouter_by_method(self) -> None: + broker = self.broker_class() + router = self.router_class() + router2 = self.router_class() + + @router2.subscriber("test") + @router2.publisher("test") + async def handle(msg) -> None: ... + + router.include_router(router2, include_in_schema=False) + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0") + + assert schema.to_jsonable()["channels"] == {} + + def test_all_nested_routers_by_method(self) -> None: + broker = self.broker_class() + router = self.router_class() + router2 = self.router_class() + + @router2.subscriber("test") + @router2.publisher("test") + async def handle(msg) -> None: ... + + router.include_router(router2) + broker.include_router(router, include_in_schema=False) + + schema = AsyncAPI(broker, schema_version="2.6.0") + + assert schema.to_jsonable()["channels"] == {} + + def test_include_subrouter(self) -> None: + broker = self.broker_class() + router = self.router_class() + router2 = self.router_class() + + @router2.subscriber("test") + @router2.publisher("test") + async def handle(msg) -> None: ... + + router.include_router(router2) + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0") + + assert len(schema.to_jsonable()["channels"]) == 2 diff --git a/tests/a_docs/confluent/ack/__init__.py b/tests/asyncapi/base/v3_0_0/__init__.py similarity index 100% rename from tests/a_docs/confluent/ack/__init__.py rename to tests/asyncapi/base/v3_0_0/__init__.py diff --git a/tests/asyncapi/base/v3_0_0/arguments.py b/tests/asyncapi/base/v3_0_0/arguments.py new file mode 100644 index 0000000000..a350ee462f --- /dev/null +++ b/tests/asyncapi/base/v3_0_0/arguments.py @@ -0,0 +1,742 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Annotated, Literal + +import pydantic +import pytest +from dirty_equals import IsDict, IsPartialDict, IsStr +from fast_depends import Depends +from fastapi import Depends as APIDepends + +from faststream import Context +from faststream._internal._compat import PYDANTIC_V2 +from faststream._internal.broker import BrokerUsecase +from faststream._internal.fastapi import StreamRouter +from faststream.specification.asyncapi import AsyncAPI +from tests.marks import pydantic_v2 + + +class FastAPICompatible: + is_fastapi: bool = False + + broker_factory: BrokerUsecase | StreamRouter + dependency_builder = staticmethod(APIDepends) + + def build_app(self, broker): + """Patch it to test FastAPI scheme generation too.""" + return broker + + def test_custom_naming(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test", title="custom_name", description="test description") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert key == "custom_name" + assert schema["channels"][key]["description"] == "test description" + + def test_slash_in_title(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test", title="/") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + assert next(iter(schema["channels"].keys())) == "." + assert schema["channels"]["."]["address"] == "/" + + assert next(iter(schema["operations"].keys())) == ".Subscribe" + + assert ( + next(iter(schema["components"]["messages"].keys())) == ".:SubscribeMessage" + ) + assert ( + schema["components"]["messages"][".:SubscribeMessage"]["title"] + == "/:SubscribeMessage" + ) + + assert next(iter(schema["components"]["schemas"].keys())) == ".:Message:Payload" + assert ( + schema["components"]["schemas"][".:Message:Payload"]["title"] + == "/:Message:Payload" + ) + + def test_docstring_description(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test", title="custom_name") + async def handle(msg) -> None: + """Test description.""" + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert key == "custom_name" + assert schema["channels"][key]["description"] == "Test description.", schema[ + "channels" + ][key]["description"] + + def test_empty(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "EmptyPayload" + assert v == { + "title": key, + "type": "null", + } + + def test_no_type(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == {"title": key} + + def test_simple_type(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(msg: int) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + assert next(iter(schema["channels"].values())).get("description") is None + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == {"title": key, "type": "integer"} + + def test_simple_optional_type(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(msg: int | None) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": key, + }, + ) | IsDict( + { # TODO: remove when deprecating PydanticV1 + "title": key, + "type": "integer", + }, + ), v + + def test_simple_type_with_default(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(msg: int = 1) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == { + "default": 1, + "title": key, + "type": "integer", + } + + def test_multi_args_no_type(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(msg, another) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == { + "properties": { + "another": {"title": "Another"}, + "msg": {"title": "Msg"}, + }, + "required": ["msg", "another"], + "title": key, + "type": "object", + } + + def test_multi_args_with_type(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(msg: str, another: int) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == { + "properties": { + "another": {"title": "Another", "type": "integer"}, + "msg": {"title": "Msg", "type": "string"}, + }, + "required": ["msg", "another"], + "title": key, + "type": "object", + } + + def test_multi_args_with_default(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(msg: str, another: int | None = None) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + + assert v == { + "properties": { + "another": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": None, + "title": "Another", + }, + ) + | IsDict( + { # TODO: remove when deprecating PydanticV1 + "title": "Another", + "type": "integer", + }, + ), + "msg": {"title": "Msg", "type": "string"}, + }, + "required": ["msg"], + "title": key, + "type": "object", + } + + def test_dataclass(self) -> None: + @dataclass + class User: + id: int + name: str = "" + + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(user: User) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "User" + assert v == { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + }, + "required": ["id"], + "title": key, + "type": "object", + } + + def test_pydantic_model(self) -> None: + class User(pydantic.BaseModel): + name: str = "" + id: int + + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(user: User) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "User" + assert v == { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + }, + "required": ["id"], + "title": key, + "type": "object", + } + + def test_pydantic_model_with_enum(self) -> None: + class Status(str, Enum): + registered = "registered" + banned = "banned" + + class User(pydantic.BaseModel): + name: str = "" + id: int + status: Status + + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(user: User) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + assert payload == { + "Status": IsPartialDict( + { + "enum": ["registered", "banned"], + "title": "Status", + "type": "string", + }, + ), + "User": { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + "status": {"$ref": "#/components/schemas/Status"}, + }, + "required": ["id", "status"], + "title": "User", + "type": "object", + }, + }, payload + + def test_pydantic_model_mixed_regular(self) -> None: + class Email(pydantic.BaseModel): + addr: str + + class User(pydantic.BaseModel): + name: str = "" + id: int + email: Email + + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(user: User, description: str = "") -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + assert payload == { + "Email": { + "title": "Email", + "type": "object", + "properties": {"addr": {"title": "Addr", "type": "string"}}, + "required": ["addr"], + }, + "User": { + "title": "User", + "type": "object", + "properties": { + "name": {"title": "Name", "default": "", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + "email": {"$ref": "#/components/schemas/Email"}, + }, + "required": ["id", "email"], + }, + "Handle:Message:Payload": { + "title": "Handle:Message:Payload", + "type": "object", + "properties": { + "user": {"$ref": "#/components/schemas/User"}, + "description": { + "title": "Description", + "default": "", + "type": "string", + }, + }, + "required": ["user"], + }, + } + + def test_pydantic_model_with_example(self) -> None: + class User(pydantic.BaseModel): + name: str = "" + id: int + + if PYDANTIC_V2: + model_config = { + "json_schema_extra": {"examples": [{"name": "john", "id": 1}]}, + } + + else: + + class Config: + schema_extra = {"examples": [{"name": "john", "id": 1}]} # noqa: RUF012 + + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(user: User) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "User" + assert v == { + "examples": [{"id": 1, "name": "john"}], + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + }, + "required": ["id"], + "title": "User", + "type": "object", + } + + def test_with_filter(self) -> None: + class User(pydantic.BaseModel): + name: str = "" + id: int + + broker = self.broker_factory() + + sub = broker.subscriber("test") + + @sub( # pragma: no branch + filter=lambda m: m.content_type == "application/json", + ) + async def handle(id: int) -> None: ... + + @sub + async def handle_default(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + assert ( + len( + next(iter(schema["components"]["messages"].values()))["payload"][ + "oneOf" + ], + ) + == 2 + ) + + payload = schema["components"]["schemas"] + + assert "Handle:Message:Payload" in list(payload.keys()) + assert "HandleDefault:Message:Payload" in list(payload.keys()) + + def test_ignores_depends(self) -> None: + broker = self.broker_factory() + + def dep(name: str = ""): + return name + + def dep2(name2: str): + return name2 + + dependencies = (self.dependency_builder(dep2),) + message = self.dependency_builder(dep) + + @broker.subscriber("test", dependencies=dependencies) + async def handle(id: int, message=message) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Handle:Message:Payload" + assert v == { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + "name2": {"title": "Name2", "type": "string"}, + }, + "required": ["id", "name2"], + "title": key, + "type": "object", + }, v + + @pydantic_v2 + def test_descriminator(self) -> None: + class Sub2(pydantic.BaseModel): + type: Literal["sub2"] + + class Sub(pydantic.BaseModel): + type: Literal["sub"] + + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle( + user: Annotated[Sub2 | Sub, pydantic.Field(discriminator="type")], + ): ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + key = next(iter(schema["components"]["messages"].keys())) + + assert key == IsStr(regex=r"test[\w:]*:Handle:SubscribeMessage"), key + + p = schema["components"]["messages"][key]["payload"] + assert p == IsPartialDict({ + "$ref": "#/components/schemas/Handle:Message:Payload", + }), p + + assert schema["components"]["schemas"] == IsPartialDict({ + "Sub": { + "properties": { + "type": IsPartialDict({"const": "sub", "title": "Type"}), + }, + "required": ["type"], + "title": "Sub", + "type": "object", + }, + "Sub2": { + "properties": { + "type": IsPartialDict({"const": "sub2", "title": "Type"}), + }, + "required": ["type"], + "title": "Sub2", + "type": "object", + }, + }), schema["components"]["schemas"] + + payload = schema["components"]["schemas"].get("Handle:Message:Payload") + + descriminator_payload = IsPartialDict({ + "discriminator": "type", + "oneOf": [ + {"$ref": "#/components/schemas/Sub2"}, + {"$ref": "#/components/schemas/Sub"}, + ], + "title": "Handle:Message:Payload", + }) + + if self.is_fastapi: + assert ( + payload + == IsPartialDict({ + "anyOf": [ + {"$ref": "#/components/schemas/Sub2"}, + {"$ref": "#/components/schemas/Sub"}, + ] + }) + | descriminator_payload + ), payload + + else: + assert payload == descriminator_payload + + @pydantic_v2 + def test_nested_descriminator(self) -> None: + class Sub2(pydantic.BaseModel): + type: Literal["sub2"] + + class Sub(pydantic.BaseModel): + type: Literal["sub"] + + class Model(pydantic.BaseModel): + msg: Sub2 | Sub = pydantic.Field(..., discriminator="type") + + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(user: Model) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + key = next(iter(schema["components"]["messages"].keys())) + assert key == IsStr(regex=r"test[\w:]*:Handle:SubscribeMessage") + assert schema["components"] == { + "messages": { + key: { + "title": key, + "correlationId": {"location": "$message.header#/correlation_id"}, + "payload": {"$ref": "#/components/schemas/Model"}, + }, + }, + "schemas": { + "Sub": { + "properties": { + "type": IsPartialDict({"const": "sub", "title": "Type"}), + }, + "required": ["type"], + "title": "Sub", + "type": "object", + }, + "Sub2": { + "properties": { + "type": IsPartialDict({"const": "sub2", "title": "Type"}), + }, + "required": ["type"], + "title": "Sub2", + "type": "object", + }, + "Model": { + "properties": { + "msg": { + "discriminator": "type", + "oneOf": [ + {"$ref": "#/components/schemas/Sub2"}, + {"$ref": "#/components/schemas/Sub"}, + ], + "title": "Msg", + }, + }, + "required": ["msg"], + "title": "Model", + "type": "object", + }, + }, + }, schema["components"] + + +class ArgumentsTestcase(FastAPICompatible): + dependency_builder = staticmethod(Depends) + + def test_pydantic_field(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("msg") + async def msg( + msg: pydantic.PositiveInt = pydantic.Field( + 1, + description="some field", + title="Perfect", + examples=[1], + ), + ) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert key == "Perfect" + + assert v == { + "default": 1, + "description": "some field", + "examples": [1], + "exclusiveMinimum": 0, + "title": "Perfect", + "type": "integer", + } + + def test_ignores_custom_field(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle( + id: int, user: str | None = None, message=Context() + ) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert v == IsDict( + { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "user": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "default": None, + "title": "User", + }, + }, + "required": ["id"], + "title": key, + "type": "object", + }, + ) | IsDict( # TODO: remove when deprecating PydanticV1 + { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "user": {"title": "User", "type": "string"}, + }, + "required": ["id"], + "title": "Handle:Message:Payload", + "type": "object", + }, + ) + + def test_overwrite_schema(self) -> None: + @dataclass + class User: + id: int + name: str = "" + + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(user: User) -> None: ... + + @dataclass + class User: + id: int + email: str = "" + + @broker.subscriber("test2") + async def second_handle(user: User) -> None: ... + + with pytest.warns( + RuntimeWarning, + match="Overwriting the message schema, data types have the same name", + ): + schema = AsyncAPI( + self.build_app(broker), schema_version="3.0.0" + ).to_jsonable() + + payload = schema["components"]["schemas"] + + assert len(payload) == 1 + + key, value = next(iter(payload.items())) + + assert key == "User" + assert value == { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "email": {"default": "", "title": "Email", "type": "string"}, + }, + "required": ["id"], + "title": key, + "type": "object", + } diff --git a/tests/asyncapi/base/v3_0_0/fastapi.py b/tests/asyncapi/base/v3_0_0/fastapi.py new file mode 100644 index 0000000000..661f344321 --- /dev/null +++ b/tests/asyncapi/base/v3_0_0/fastapi.py @@ -0,0 +1,142 @@ +from collections.abc import Callable +from typing import Any + +import pytest +from dirty_equals import IsStr +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from faststream._internal.broker import BrokerUsecase +from faststream._internal.fastapi.router import StreamRouter +from faststream._internal.types import MsgType +from faststream.specification.asyncapi import AsyncAPI + + +class FastAPITestCase: + is_fastapi = True + + router_factory: type[StreamRouter[MsgType]] + broker_wrapper: Callable[[BrokerUsecase[MsgType, Any]], BrokerUsecase[MsgType, Any]] + + @pytest.mark.asyncio() + async def test_fastapi_full_information(self) -> None: + broker = self.router_factory( + protocol="custom", + protocol_version="1.1.1", + description="Test broker description", + schema_url="/asyncapi_schema", + specification_tags=[{"name": "test"}], + ) + + app = FastAPI( + title="CustomApp", + version="1.1.1", + description="Test description", + contact={"name": "support", "url": "https://support.com"}, + license_info={"name": "some", "url": "https://some.com"}, + ) + app.include_router(broker) + + async with self.broker_wrapper(broker.broker): + with TestClient(app) as client: + response_json = client.get("/asyncapi_schema.json") + + assert response_json.json() == { + "asyncapi": "3.0.0", + "defaultContentType": "application/json", + "info": { + "title": "CustomApp", + "version": "1.1.1", + "description": "Test description", + "contact": { + "name": "support", + "url": IsStr(regex=r"https\:\/\/support\.com\/?"), + }, + "license": { + "name": "some", + "url": IsStr(regex=r"https\:\/\/some\.com\/?"), + }, + }, + "servers": { + "development": { + "host": IsStr(), + "pathname": IsStr(), + "protocol": "custom", + "description": "Test broker description", + "protocolVersion": "1.1.1", + "tags": [{"name": "test"}], + }, + }, + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + }, response_json.json() + + @pytest.mark.asyncio() + async def test_fastapi_asyncapi_routes(self) -> None: + router = self.router_factory(schema_url="/asyncapi_schema") + + @router.subscriber("test") + async def handler() -> None: ... + + app = FastAPI() + app.include_router(router) + + async with self.broker_wrapper(router.broker): + with TestClient(app) as client: + schema = AsyncAPI( + router.broker, + title=router.title, + description=router.description, + app_version=router.version, + contact=router.contact, + license=router.license, + schema_version="3.0.0", + ) + + response_json = client.get("/asyncapi_schema.json") + assert response_json.json() == schema.to_jsonable(), ( + schema.to_jsonable() + ) + + response_yaml = client.get("/asyncapi_schema.yaml") + assert response_yaml.text == schema.to_yaml() + + response_html = client.get("/asyncapi_schema") + assert response_html.status_code == 200 + + @pytest.mark.asyncio() + async def test_fastapi_asyncapi_not_fount(self) -> None: + broker = self.router_factory(include_in_schema=False) + + app = FastAPI() + app.include_router(broker) + + async with self.broker_wrapper(broker.broker): + with TestClient(app) as client: + response_json = client.get("/asyncapi.json") + assert response_json.status_code == 404 + + response_yaml = client.get("/asyncapi.yaml") + assert response_yaml.status_code == 404 + + response_html = client.get("/asyncapi") + assert response_html.status_code == 404 + + @pytest.mark.asyncio() + async def test_fastapi_asyncapi_not_fount_by_url(self) -> None: + broker = self.router_factory(schema_url=None) + + app = FastAPI() + app.include_router(broker) + + async with self.broker_wrapper(broker.broker): + with TestClient(app) as client: + response_json = client.get("/asyncapi.json") + assert response_json.status_code == 404 + + response_yaml = client.get("/asyncapi.yaml") + assert response_yaml.status_code == 404 + + response_html = client.get("/asyncapi") + assert response_html.status_code == 404 diff --git a/tests/asyncapi/base/v3_0_0/naming.py b/tests/asyncapi/base/v3_0_0/naming.py new file mode 100644 index 0000000000..d2c9ce9c49 --- /dev/null +++ b/tests/asyncapi/base/v3_0_0/naming.py @@ -0,0 +1,413 @@ +from typing import Any + +from dirty_equals import Contains, IsStr +from pydantic import create_model + +from faststream._internal.broker import BrokerUsecase +from faststream.specification.asyncapi import AsyncAPI + + +class BaseNaming: + broker_class: type[BrokerUsecase[Any, Any]] + + +class SubscriberNaming(BaseNaming): + def test_subscriber_naming(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle_user_created(msg: str) -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated:SubscribeMessage"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Message:Payload", + ] + + def test_pydantic_subscriber_naming(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle_user_created(msg: create_model("SimpleModel")) -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated:SubscribeMessage"), + ] + + assert list(schema["components"]["schemas"].keys()) == ["SimpleModel"] + + def test_multi_subscribers_naming(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + @broker.subscriber("test2") + async def handle_user_created(msg: str) -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated"), + IsStr(regex=r"test2[\w:]*:HandleUserCreated"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated:SubscribeMessage"), + IsStr(regex=r"test2[\w:]*:HandleUserCreated:SubscribeMessage"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Message:Payload", + ] + + def test_subscriber_naming_manual(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test", title="custom") + async def handle_user_created(msg: str) -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == [ + "custom:SubscribeMessage", + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "custom:Message:Payload", + ] + + def test_subscriber_naming_default(self) -> None: + broker = self.broker_class() + + broker.subscriber("test") + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:Subscriber"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:Subscriber:SubscribeMessage"), + ] + + for key, v in schema["components"]["schemas"].items(): + assert key == "Subscriber:Message:Payload" + assert v == {"title": key} + + def test_subscriber_naming_default_with_title(self) -> None: + broker = self.broker_class() + + broker.subscriber("test", title="custom") + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == [ + "custom:SubscribeMessage", + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "custom:Message:Payload", + ] + + assert schema["components"]["schemas"]["custom:Message:Payload"] == { + "title": "custom:Message:Payload", + } + + def test_multi_subscribers_naming_default(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle_user_created(msg: str) -> None: ... + + broker.subscriber("test2") + broker.subscriber("test3") + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated"), + IsStr(regex=r"test2[\w:]*:Subscriber"), + IsStr(regex=r"test3[\w:]*:Subscriber"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:HandleUserCreated:SubscribeMessage"), + IsStr(regex=r"test2[\w:]*:Subscriber:SubscribeMessage"), + IsStr(regex=r"test3[\w:]*:Subscriber:SubscribeMessage"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Message:Payload", + "Subscriber:Message:Payload", + ] + + assert schema["components"]["schemas"]["Subscriber:Message:Payload"] == { + "title": "Subscriber:Message:Payload", + } + + +class FilterNaming(BaseNaming): + def test_subscriber_filter_base(self) -> None: + broker = self.broker_class() + + sub = broker.subscriber("test") + + @sub + async def handle_user_created(msg: str) -> None: ... + + @sub + async def handle_user_id(msg: int) -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr( + regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]:SubscribeMessage" + ), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Message:Payload", + "HandleUserId:Message:Payload", + ] + + def test_subscriber_filter_pydantic(self) -> None: + broker = self.broker_class() + + sub = broker.subscriber("test") + + @sub + async def handle_user_created(msg: create_model("SimpleModel")) -> None: ... + + @sub + async def handle_user_id(msg: int) -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr( + regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]:SubscribeMessage" + ), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "SimpleModel", + "HandleUserId:Message:Payload", + ] + + def test_subscriber_filter_with_title(self) -> None: + broker = self.broker_class() + + sub = broker.subscriber("test", title="custom") + + @sub + async def handle_user_created(msg: str) -> None: ... + + @sub + async def handle_user_id(msg: int) -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == [ + "custom:SubscribeMessage", + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Message:Payload", + "HandleUserId:Message:Payload", + ] + + +class PublisherNaming(BaseNaming): + def test_publisher_naming_base(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + async def handle_user_created() -> str: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == [IsStr(regex=r"test[\w:]*:Publisher")] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message:Payload"), + ] + + def test_publisher_naming_pydantic(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + async def handle_user_created() -> create_model("SimpleModel"): ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == [IsStr(regex=r"test[\w:]*:Publisher")] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "SimpleModel", + ], list(schema["components"]["schemas"].keys()) + + def test_publisher_manual_naming(self) -> None: + broker = self.broker_class() + + @broker.publisher("test", title="custom") + async def handle_user_created() -> str: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == ["custom:Message"] + + assert list(schema["components"]["schemas"].keys()) == [ + "custom:Message:Payload", + ] + + def test_publisher_with_schema_naming(self) -> None: + broker = self.broker_class() + + @broker.publisher("test", schema=str) + async def handle_user_created() -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == [IsStr(regex=r"test[\w:]*:Publisher")] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message:Payload"), + ] + + def test_publisher_manual_naming_with_schema(self) -> None: + broker = self.broker_class() + + @broker.publisher("test", title="custom", schema=str) + async def handle_user_created() -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == ["custom:Message"] + + assert list(schema["components"]["schemas"].keys()) == [ + "custom:Message:Payload", + ] + + def test_multi_publishers_naming(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + @broker.publisher("test2") + async def handle_user_created() -> str: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + names = list(schema["channels"].keys()) + assert names == Contains( + IsStr(regex=r"test2[\w:]*:Publisher"), + IsStr(regex=r"test[\w:]*:Publisher"), + ), names + + messages = list(schema["components"]["messages"].keys()) + assert messages == Contains( + IsStr(regex=r"test2[\w:]*:Publisher:Message"), + IsStr(regex=r"test[\w:]*:Publisher:Message"), + ), messages + + payloads = list(schema["components"]["schemas"].keys()) + assert payloads == Contains( + IsStr(regex=r"test2[\w:]*:Publisher:Message:Payload"), + IsStr(regex=r"test[\w:]*:Publisher:Message:Payload"), + ), payloads + + def test_multi_publisher_usages(self) -> None: + broker = self.broker_class() + + pub = broker.publisher("test") + + @pub + async def handle_user_created() -> str: ... + + @pub + async def handle() -> int: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher"), + ] + + assert list(schema["components"]["messages"].keys()) == [ + IsStr(regex=r"test[\w:]*:Publisher:Message"), + ] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Publisher:Message:Payload", + "Handle:Publisher:Message:Payload", + ], list(schema["components"]["schemas"].keys()) + + def test_multi_publisher_usages_with_custom(self) -> None: + broker = self.broker_class() + + pub = broker.publisher("test", title="custom") + + @pub + async def handle_user_created() -> str: ... + + @pub + async def handle() -> int: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["custom"] + + assert list(schema["components"]["messages"].keys()) == ["custom:Message"] + + assert list(schema["components"]["schemas"].keys()) == [ + "HandleUserCreated:Publisher:Message:Payload", + "Handle:Publisher:Message:Payload", + ] + + +class NamingTestCase(SubscriberNaming, FilterNaming, PublisherNaming): + pass diff --git a/tests/asyncapi/base/v3_0_0/publisher.py b/tests/asyncapi/base/v3_0_0/publisher.py new file mode 100644 index 0000000000..80c9775aba --- /dev/null +++ b/tests/asyncapi/base/v3_0_0/publisher.py @@ -0,0 +1,126 @@ +import pydantic + +from faststream._internal.broker import BrokerUsecase +from faststream._internal.fastapi import StreamRouter +from faststream.specification.asyncapi import AsyncAPI + + +class PublisherTestcase: + broker_factory: BrokerUsecase | StreamRouter + + def build_app(self, broker): + """Patch it to test FastAPI scheme generation too.""" + return broker + + def test_publisher_with_description(self) -> None: + broker = self.broker_factory() + + @broker.publisher("test", description="test description") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + assert schema["channels"][key]["description"] == "test description" + + def test_basic_publisher(self) -> None: + broker = self.broker_factory() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + assert schema["channels"][key].get("description") is None + assert schema["operations"][key] is not None + + payload = schema["components"]["schemas"] + for v in payload.values(): + assert v == {} + + def test_none_publisher(self) -> None: + broker = self.broker_factory() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + for v in payload.values(): + assert v == {} + + def test_typed_publisher(self) -> None: + broker = self.broker_factory() + + @broker.publisher("test") + async def handle(msg) -> int: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + for v in payload.values(): + assert v["type"] == "integer" + + def test_pydantic_model_publisher(self) -> None: + class User(pydantic.BaseModel): + name: str = "" + id: int + + broker = self.broker_factory() + + @broker.publisher("test") + async def handle(msg) -> User: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + + for key, v in payload.items(): + assert v == { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + }, + "required": ["id"], + "title": key, + "type": "object", + } + + def test_delayed(self) -> None: + broker = self.broker_factory() + + pub = broker.publisher("test") + + @pub + async def handle(msg) -> int: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + for v in payload.values(): + assert v["type"] == "integer" + + def test_with_schema(self) -> None: + broker = self.broker_factory() + + broker.publisher("test", title="Custom", schema=int) + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + for v in payload.values(): + assert v["type"] == "integer" + + def test_not_include(self) -> None: + broker = self.broker_factory() + + @broker.publisher("test", include_in_schema=False) + @broker.subscriber("in-test", include_in_schema=False) + async def handler(msg: str) -> None: + pass + + schema = AsyncAPI(self.build_app(broker)) + + assert schema.to_jsonable()["channels"] == {}, schema.to_jsonable()["channels"] diff --git a/tests/asyncapi/base/v3_0_0/router.py b/tests/asyncapi/base/v3_0_0/router.py new file mode 100644 index 0000000000..347e56ba74 --- /dev/null +++ b/tests/asyncapi/base/v3_0_0/router.py @@ -0,0 +1,166 @@ +from dirty_equals import IsStr + +from faststream._internal.broker import BrokerUsecase +from faststream._internal.broker.router import ( + ArgsContainer, + BrokerRouter, + SubscriberRoute, +) +from faststream.specification.asyncapi import AsyncAPI + + +class RouterTestcase: + broker_class: type[BrokerUsecase] + router_class: type[BrokerRouter] + publisher_class: type[ArgsContainer] + route_class: type[SubscriberRoute] + + def test_delay_subscriber(self) -> None: + broker = self.broker_class() + + async def handle(msg) -> None: ... + + router = self.router_class( + handlers=(self.route_class(handle, "test"),), + ) + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + payload = schema["components"]["schemas"] + key = list(payload.keys())[0] # noqa: RUF015 + assert payload[key]["title"] == key == "Handle:Message:Payload" + + def test_delay_publisher(self) -> None: + broker = self.broker_class() + + async def handle(msg) -> None: ... + + router = self.router_class( + handlers=( + self.route_class( + handle, + "test", + publishers=(self.publisher_class("test2", schema=int),), + ), + ), + ) + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="3.0.0") + schemas = schema.to_jsonable()["components"]["schemas"] + del schemas["Handle:Message:Payload"] + + for i, j in schemas.items(): + assert ( + i == j["title"] == IsStr(regex=r"test2[\w:]*:Publisher:Message:Payload") + ) + assert j["type"] == "integer" + + def test_not_include(self) -> None: + broker = self.broker_class() + router = self.router_class(include_in_schema=False) + + @router.subscriber("test") + @router.publisher("test") + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="3.0.0") + assert schema.to_jsonable()["channels"] == {}, schema.to_jsonable()["channels"] + + def test_not_include_in_method(self) -> None: + broker = self.broker_class() + router = self.router_class() + + @router.subscriber("test") + @router.publisher("test") + async def handle(msg) -> None: ... + + broker.include_router(router, include_in_schema=False) + + schema = AsyncAPI(broker, schema_version="3.0.0") + assert schema.to_jsonable()["channels"] == {}, schema.to_jsonable()["channels"] + + def test_respect_subrouter(self) -> None: + broker = self.broker_class() + router = self.router_class() + router2 = self.router_class(include_in_schema=False) + + @router2.subscriber("test") + @router2.publisher("test") + async def handle(msg) -> None: ... + + router.include_router(router2) + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="3.0.0") + + assert schema.to_jsonable()["channels"] == {}, schema.to_jsonable()["channels"] + + def test_not_include_subrouter(self) -> None: + broker = self.broker_class() + router = self.router_class(include_in_schema=False) + router2 = self.router_class() + + @router2.subscriber("test") + @router2.publisher("test") + async def handle(msg) -> None: ... + + router.include_router(router2) + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="3.0.0") + + assert schema.to_jsonable()["channels"] == {} + + def test_not_include_subrouter_by_method(self) -> None: + broker = self.broker_class() + router = self.router_class() + router2 = self.router_class() + + @router2.subscriber("test") + @router2.publisher("test") + async def handle(msg) -> None: ... + + router.include_router(router2, include_in_schema=False) + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="3.0.0") + + assert schema.to_jsonable()["channels"] == {} + + def test_all_nested_routers_by_method(self) -> None: + broker = self.broker_class() + router = self.router_class() + router2 = self.router_class() + + @router2.subscriber("test") + @router2.publisher("test") + async def handle(msg) -> None: ... + + router.include_router(router2) + broker.include_router(router, include_in_schema=False) + + schema = AsyncAPI(broker, schema_version="3.0.0") + + assert schema.to_jsonable()["channels"] == {} + + def test_include_subrouter(self) -> None: + broker = self.broker_class() + router = self.router_class() + router2 = self.router_class() + + @router2.subscriber("test") + @router2.publisher("test") + async def handle(msg) -> None: ... + + router.include_router(router2) + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="3.0.0") + + assert len(schema.to_jsonable()["channels"]) == 2 diff --git a/tests/asyncapi/confluent/security.py b/tests/asyncapi/confluent/security.py new file mode 100644 index 0000000000..362b5f01d5 --- /dev/null +++ b/tests/asyncapi/confluent/security.py @@ -0,0 +1,138 @@ +import pytest +from dirty_equals import IsPartialDict + +from faststream.confluent import KafkaBroker +from faststream.security import ( + SASLGSSAPI, + BaseSecurity, + SASLOAuthBearer, + SASLPlaintext, + SASLScram256, + SASLScram512, +) +from faststream.specification.base.specification import Specification + + +class SecurityTestcase: + def get_schema(self, broker: KafkaBroker) -> Specification: + raise NotImplementedError + + @pytest.mark.parametrize( + ("security", "schema"), + ( + pytest.param( + BaseSecurity(use_ssl=True), + IsPartialDict({ + "servers": IsPartialDict({ + "development": IsPartialDict({ + "protocol": "kafka-secure", + "protocolVersion": "auto", + "security": [], + }) + }) + }), + id="BaseSecurity", + ), + pytest.param( + SASLPlaintext( + username="admin", + password="password", # pragma: allowlist secret + use_ssl=True, + ), + IsPartialDict({ + "servers": IsPartialDict({ + "development": IsPartialDict({ + "protocol": "kafka-secure", + "security": [{"user-password": []}], + }) + }), + "components": IsPartialDict({ + "securitySchemes": { + "user-password": {"type": "userPassword"}, + } + }), + }), + id="SASLPlaintext", + ), + pytest.param( + SASLScram256( + username="admin", + password="password", # pragma: allowlist secret + use_ssl=True, + ), + IsPartialDict({ + "servers": IsPartialDict({ + "development": IsPartialDict({ + "protocol": "kafka-secure", + "security": [{"scram256": []}], + }) + }), + "components": IsPartialDict({ + "securitySchemes": { + "scram256": {"type": "scramSha256"}, + } + }), + }), + id="SASLScram256", + ), + pytest.param( + SASLScram512( + username="admin", + password="password", # pragma: allowlist secret + use_ssl=True, + ), + IsPartialDict({ + "servers": IsPartialDict({ + "development": IsPartialDict({ + "protocol": "kafka-secure", + "security": [{"scram512": []}], + }) + }), + "components": IsPartialDict({ + "securitySchemes": { + "scram512": {"type": "scramSha512"}, + } + }), + }), + id="SASLScram512", + ), + pytest.param( + SASLOAuthBearer(use_ssl=True), + IsPartialDict({ + "servers": IsPartialDict({ + "development": IsPartialDict({ + "protocol": "kafka-secure", + "security": [{"oauthbearer": []}], + }) + }), + "components": IsPartialDict({ + "securitySchemes": { + "oauthbearer": {"type": "oauth2", "$ref": ""} + } + }), + }), + id="SASLOAuthBearer", + ), + pytest.param( + SASLGSSAPI(use_ssl=True), + IsPartialDict({ + "servers": IsPartialDict({ + "development": IsPartialDict({ + "protocol": "kafka-secure", + "security": [{"gssapi": []}], + }) + }), + "components": IsPartialDict({ + "securitySchemes": {"gssapi": {"type": "gssapi"}} + }), + }), + id="SASLGSSAPI", + ), + ), + ) + def test_security_schema( + self, security: BaseSecurity, schema: dict[str, str] + ) -> None: + broker = KafkaBroker(security=security) + generated_schema = self.get_schema(broker) + assert generated_schema.to_jsonable() == schema diff --git a/tests/asyncapi/confluent/test_arguments.py b/tests/asyncapi/confluent/test_arguments.py deleted file mode 100644 index 2eb5c91cd1..0000000000 --- a/tests/asyncapi/confluent/test_arguments.py +++ /dev/null @@ -1,56 +0,0 @@ -from faststream.asyncapi.generate import get_app_schema -from faststream.confluent import KafkaBroker, TopicPartition -from tests.asyncapi.base.arguments import ArgumentsTestcase - - -class TestArguments(ArgumentsTestcase): - broker_class = KafkaBroker - - def test_subscriber_bindings(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "kafka": {"bindingVersion": "0.4.0", "topic": "test"} - } - - def test_subscriber_with_one_topic_partitions(self): - broker = self.broker_class() - - part1 = TopicPartition("topic_name", 1) - part2 = TopicPartition("topic_name", 2) - - @broker.subscriber(partitions=[part1, part2]) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "kafka": {"bindingVersion": "0.4.0", "topic": "topic_name"} - } - - def test_subscriber_with_multi_topics_partitions(self): - broker = self.broker_class() - - part1 = TopicPartition("topic_name1", 1) - part2 = TopicPartition("topic_name2", 2) - - @broker.subscriber(partitions=[part1, part2]) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key1 = tuple(schema["channels"].keys())[0] # noqa: RUF015 - key2 = tuple(schema["channels"].keys())[1] - - assert sorted( - ( - schema["channels"][key1]["bindings"]["kafka"]["topic"], - schema["channels"][key2]["bindings"]["kafka"]["topic"], - ) - ) == sorted(("topic_name1", "topic_name2")) diff --git a/tests/asyncapi/confluent/test_connection.py b/tests/asyncapi/confluent/test_connection.py deleted file mode 100644 index d37cefbfa7..0000000000 --- a/tests/asyncapi/confluent/test_connection.py +++ /dev/null @@ -1,92 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.asyncapi.schema import Tag -from faststream.confluent import KafkaBroker - - -def test_base(): - schema = get_app_schema( - FastStream( - KafkaBroker( - "kafka:9092", - protocol="plaintext", - protocol_version="0.9.0", - description="Test description", - tags=(Tag(name="some-tag", description="experimental"),), - ) - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "description": "Test description", - "protocol": "plaintext", - "protocolVersion": "0.9.0", - "tags": [{"description": "experimental", "name": "some-tag"}], - "url": "kafka:9092", - } - }, - } - - -def test_multi(): - schema = get_app_schema( - FastStream(KafkaBroker(["kafka:9092", "kafka:9093"])) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "Server1": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "kafka:9092", - }, - "Server2": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "kafka:9093", - }, - }, - } - - -def test_custom(): - schema = get_app_schema( - FastStream( - KafkaBroker( - ["kafka:9092", "kafka:9093"], - asyncapi_url=["kafka:9094", "kafka:9095"], - ) - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "Server1": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "kafka:9094", - }, - "Server2": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "kafka:9095", - }, - }, - } diff --git a/tests/asyncapi/confluent/test_fastapi.py b/tests/asyncapi/confluent/test_fastapi.py deleted file mode 100644 index adf7b5cb28..0000000000 --- a/tests/asyncapi/confluent/test_fastapi.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Type - -from faststream.asyncapi.generate import get_app_schema -from faststream.confluent.fastapi import KafkaRouter -from faststream.confluent.testing import TestKafkaBroker -from faststream.security import SASLPlaintext -from tests.asyncapi.base.arguments import FastAPICompatible -from tests.asyncapi.base.fastapi import FastAPITestCase -from tests.asyncapi.base.publisher import PublisherTestcase - - -class TestRouterArguments(FastAPITestCase, FastAPICompatible): - broker_class: Type[KafkaRouter] = KafkaRouter - broker_wrapper = staticmethod(TestKafkaBroker) - - def build_app(self, router): - return router - - -class TestRouterPublisher(PublisherTestcase): - broker_class = KafkaRouter - - def build_app(self, router): - return router - - -def test_fastapi_security_schema(): - security = SASLPlaintext(username="user", password="pass", use_ssl=False) - - broker = KafkaRouter("localhost:9092", security=security) - - schema = get_app_schema(broker).to_jsonable() - - assert schema["servers"]["development"] == { - "protocol": "kafka", - "protocolVersion": "auto", - "security": [{"user-password": []}], - "url": "localhost:9092", - } - assert schema["components"]["securitySchemes"] == { - "user-password": {"type": "userPassword"} - } diff --git a/tests/asyncapi/confluent/test_naming.py b/tests/asyncapi/confluent/test_naming.py deleted file mode 100644 index cdcd3dc17c..0000000000 --- a/tests/asyncapi/confluent/test_naming.py +++ /dev/null @@ -1,50 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.confluent import KafkaBroker -from tests.asyncapi.base.naming import NamingTestCase - - -class TestNaming(NamingTestCase): - broker_class = KafkaBroker - - def test_base(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "defaultContentType": "application/json", - "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, - "servers": { - "development": { - "url": "localhost", - "protocol": "kafka", - "protocolVersion": "auto", - } - }, - "channels": { - "test:Handle": { - "servers": ["development"], - "bindings": {"kafka": {"topic": "test", "bindingVersion": "0.4.0"}}, - "subscribe": { - "message": {"$ref": "#/components/messages/test:Handle:Message"} - }, - } - }, - "components": { - "messages": { - "test:Handle:Message": { - "title": "test:Handle:Message", - "correlationId": { - "location": "$message.header#/correlation_id" - }, - "payload": {"$ref": "#/components/schemas/EmptyPayload"}, - } - }, - "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, - }, - } diff --git a/tests/asyncapi/confluent/test_publisher.py b/tests/asyncapi/confluent/test_publisher.py deleted file mode 100644 index b6ee208854..0000000000 --- a/tests/asyncapi/confluent/test_publisher.py +++ /dev/null @@ -1,20 +0,0 @@ -from faststream.asyncapi.generate import get_app_schema -from faststream.confluent import KafkaBroker -from tests.asyncapi.base.publisher import PublisherTestcase - - -class TestArguments(PublisherTestcase): - broker_class = KafkaBroker - - def test_publisher_bindings(self): - broker = self.broker_class() - - @broker.publisher("test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "kafka": {"bindingVersion": "0.4.0", "topic": "test"} - } diff --git a/tests/asyncapi/confluent/test_router.py b/tests/asyncapi/confluent/test_router.py deleted file mode 100644 index 44424190d8..0000000000 --- a/tests/asyncapi/confluent/test_router.py +++ /dev/null @@ -1,85 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.confluent import KafkaBroker, KafkaPublisher, KafkaRoute, KafkaRouter -from tests.asyncapi.base.arguments import ArgumentsTestcase -from tests.asyncapi.base.publisher import PublisherTestcase -from tests.asyncapi.base.router import RouterTestcase - - -class TestRouter(RouterTestcase): - broker_class = KafkaBroker - router_class = KafkaRouter - route_class = KafkaRoute - publisher_class = KafkaPublisher - - def test_prefix(self): - broker = self.broker_class() - - router = self.router_class(prefix="test_") - - @router.subscriber("test") - async def handle(msg): ... - - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "defaultContentType": "application/json", - "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, - "servers": { - "development": { - "url": "localhost", - "protocol": "kafka", - "protocolVersion": "auto", - } - }, - "channels": { - "test_test:Handle": { - "servers": ["development"], - "bindings": { - "kafka": {"topic": "test_test", "bindingVersion": "0.4.0"} - }, - "subscribe": { - "message": { - "$ref": "#/components/messages/test_test:Handle:Message" - } - }, - } - }, - "components": { - "messages": { - "test_test:Handle:Message": { - "title": "test_test:Handle:Message", - "correlationId": { - "location": "$message.header#/correlation_id" - }, - "payload": { - "$ref": "#/components/schemas/Handle:Message:Payload" - }, - } - }, - "schemas": { - "Handle:Message:Payload": {"title": "Handle:Message:Payload"} - }, - }, - } - - -class TestRouterArguments(ArgumentsTestcase): - broker_class = KafkaRouter - - def build_app(self, router): - broker = KafkaBroker() - broker.include_router(router) - return FastStream(broker) - - -class TestRouterPublisher(PublisherTestcase): - broker_class = KafkaRouter - - def build_app(self, router): - broker = KafkaBroker() - broker.include_router(router) - return FastStream(broker) diff --git a/tests/asyncapi/confluent/test_security.py b/tests/asyncapi/confluent/test_security.py deleted file mode 100644 index 915621ab84..0000000000 --- a/tests/asyncapi/confluent/test_security.py +++ /dev/null @@ -1,221 +0,0 @@ -import ssl -from copy import deepcopy - -from faststream.app import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.confluent import KafkaBroker -from faststream.security import ( - SASLGSSAPI, - BaseSecurity, - SASLOAuthBearer, - SASLPlaintext, - SASLScram256, - SASLScram512, -) - -basic_schema = { - "asyncapi": "2.6.0", - "channels": { - "test_1:TestTopic": { - "bindings": {"kafka": {"bindingVersion": "0.4.0", "topic": "test_1"}}, - "servers": ["development"], - "subscribe": { - "message": {"$ref": "#/components/messages/test_1:TestTopic:Message"} - }, - }, - "test_2:Publisher": { - "bindings": {"kafka": {"bindingVersion": "0.4.0", "topic": "test_2"}}, - "publish": { - "message": {"$ref": "#/components/messages/test_2:Publisher:Message"} - }, - "servers": ["development"], - }, - }, - "components": { - "messages": { - "test_1:TestTopic:Message": { - "correlationId": {"location": "$message.header#/correlation_id"}, - "payload": {"$ref": "#/components/schemas/TestTopic:Message:Payload"}, - "title": "test_1:TestTopic:Message", - }, - "test_2:Publisher:Message": { - "correlationId": {"location": "$message.header#/correlation_id"}, - "payload": { - "$ref": "#/components/schemas/test_2:Publisher:Message:Payload" - }, - "title": "test_2:Publisher:Message", - }, - }, - "schemas": { - "TestTopic:Message:Payload": { - "title": "TestTopic:Message:Payload", - "type": "string", - }, - "test_2:Publisher:Message:Payload": { - "title": "test_2:Publisher:Message:Payload", - "type": "string", - }, - }, - "securitySchemes": {}, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "kafka-secure", - "protocolVersion": "auto", - "security": [], - "url": "localhost:9092", - } - }, -} - - -def test_base_security_schema(): - ssl_context = ssl.create_default_context() - security = BaseSecurity(ssl_context=ssl_context) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - assert schema == basic_schema - - -def test_plaintext_security_schema(): - ssl_context = ssl.create_default_context() - security = SASLPlaintext( - ssl_context=ssl_context, - username="admin", - password="password", # pragma: allowlist secret - ) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - plaintext_security_schema = deepcopy(basic_schema) - plaintext_security_schema["servers"]["development"]["security"] = [ - {"user-password": []} - ] - plaintext_security_schema["components"]["securitySchemes"] = { - "user-password": {"type": "userPassword"} - } - - assert schema == plaintext_security_schema - - -def test_scram256_security_schema(): - ssl_context = ssl.create_default_context() - security = SASLScram256( - ssl_context=ssl_context, - username="admin", - password="password", # pragma: allowlist secret - ) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - sasl256_security_schema = deepcopy(basic_schema) - sasl256_security_schema["servers"]["development"]["security"] = [{"scram256": []}] - sasl256_security_schema["components"]["securitySchemes"] = { - "scram256": {"type": "scramSha256"} - } - - assert schema == sasl256_security_schema - - -def test_scram512_security_schema(): - ssl_context = ssl.create_default_context() - security = SASLScram512( - ssl_context=ssl_context, - username="admin", - password="password", # pragma: allowlist secret - ) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - sasl512_security_schema = deepcopy(basic_schema) - sasl512_security_schema["servers"]["development"]["security"] = [{"scram512": []}] - sasl512_security_schema["components"]["securitySchemes"] = { - "scram512": {"type": "scramSha512"} - } - - assert schema == sasl512_security_schema - - -def test_oauthbearer_security_schema(): - ssl_context = ssl.create_default_context() - security = SASLOAuthBearer( - ssl_context=ssl_context, - ) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - sasl_oauthbearer_security_schema = deepcopy(basic_schema) - sasl_oauthbearer_security_schema["servers"]["development"]["security"] = [ - {"oauthbearer": []} - ] - sasl_oauthbearer_security_schema["components"]["securitySchemes"] = { - "oauthbearer": {"type": "oauth2", "$ref": ""} - } - - assert schema == sasl_oauthbearer_security_schema - - -def test_gssapi_security_schema(): - ssl_context = ssl.create_default_context() - security = SASLGSSAPI(ssl_context=ssl_context) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - gssapi_security_schema = deepcopy(basic_schema) - gssapi_security_schema["servers"]["development"]["security"] = [{"gssapi": []}] - gssapi_security_schema["components"]["securitySchemes"] = { - "gssapi": {"type": "gssapi"} - } - - assert schema == gssapi_security_schema diff --git a/tests/a_docs/confluent/__init__.py b/tests/asyncapi/confluent/v2_6_0/__init__.py similarity index 100% rename from tests/a_docs/confluent/__init__.py rename to tests/asyncapi/confluent/v2_6_0/__init__.py diff --git a/tests/asyncapi/confluent/v2_6_0/base.py b/tests/asyncapi/confluent/v2_6_0/base.py new file mode 100644 index 0000000000..99989b3019 --- /dev/null +++ b/tests/asyncapi/confluent/v2_6_0/base.py @@ -0,0 +1,8 @@ +from faststream.confluent import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from faststream.specification.base.specification import Specification + + +class AsyncAPI26Mixin: + def get_schema(self, broker: KafkaBroker) -> Specification: + return AsyncAPI(broker, schema_version="2.6.0") diff --git a/tests/asyncapi/confluent/v2_6_0/test_arguments.py b/tests/asyncapi/confluent/v2_6_0/test_arguments.py new file mode 100644 index 0000000000..4bc16826e8 --- /dev/null +++ b/tests/asyncapi/confluent/v2_6_0/test_arguments.py @@ -0,0 +1,54 @@ +from faststream.confluent import KafkaBroker, TopicPartition +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase + + +class TestArguments(ArgumentsTestcase): + broker_class = KafkaBroker + + def test_subscriber_bindings(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "kafka": {"bindingVersion": "0.4.0", "topic": "test"}, + } + + def test_subscriber_with_one_topic_partitions(self) -> None: + broker = self.broker_class() + + part1 = TopicPartition("topic_name", 1) + part2 = TopicPartition("topic_name", 2) + + @broker.subscriber(partitions=[part1, part2]) + async def handle(msg): ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "kafka": {"bindingVersion": "0.4.0", "topic": "topic_name"} + } + + def test_subscriber_with_multi_topics_partitions(self) -> None: + broker = self.broker_class() + + part1 = TopicPartition("topic_name1", 1) + part2 = TopicPartition("topic_name2", 2) + + @broker.subscriber(partitions=[part1, part2]) + async def handle(msg): ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key1 = tuple(schema["channels"].keys())[0] # noqa: RUF015 + key2 = tuple(schema["channels"].keys())[1] + + assert sorted(( + schema["channels"][key1]["bindings"]["kafka"]["topic"], + schema["channels"][key2]["bindings"]["kafka"]["topic"], + )) == sorted(("topic_name1", "topic_name2")) diff --git a/tests/asyncapi/confluent/v2_6_0/test_connection.py b/tests/asyncapi/confluent/v2_6_0/test_connection.py new file mode 100644 index 0000000000..368bbc00dd --- /dev/null +++ b/tests/asyncapi/confluent/v2_6_0/test_connection.py @@ -0,0 +1,90 @@ +from faststream.confluent import KafkaBroker +from faststream.specification import Tag +from faststream.specification.asyncapi import AsyncAPI + + +def test_base() -> None: + schema = AsyncAPI( + KafkaBroker( + "kafka:9092", + protocol="plaintext", + protocol_version="0.9.0", + description="Test description", + tags=(Tag(name="some-tag", description="experimental"),), + ), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "description": "Test description", + "protocol": "plaintext", + "protocolVersion": "0.9.0", + "tags": [{"description": "experimental", "name": "some-tag"}], + "url": "kafka:9092", + }, + }, + } + + +def test_multi() -> None: + schema = AsyncAPI( + KafkaBroker(["kafka:9092", "kafka:9093"]), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "kafka:9092", + }, + "Server2": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "kafka:9093", + }, + }, + } + + +def test_custom() -> None: + schema = AsyncAPI( + KafkaBroker( + ["kafka:9092", "kafka:9093"], + specification_url=["kafka:9094", "kafka:9095"], + ), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "kafka:9094", + }, + "Server2": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "kafka:9095", + }, + }, + } diff --git a/tests/asyncapi/confluent/v2_6_0/test_fastapi.py b/tests/asyncapi/confluent/v2_6_0/test_fastapi.py new file mode 100644 index 0000000000..30fb263bd2 --- /dev/null +++ b/tests/asyncapi/confluent/v2_6_0/test_fastapi.py @@ -0,0 +1,41 @@ +from faststream.confluent.fastapi import KafkaRouter +from faststream.confluent.testing import TestKafkaBroker +from faststream.security import SASLPlaintext +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import FastAPICompatible +from tests.asyncapi.base.v2_6_0.fastapi import FastAPITestCase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase + + +class TestRouterArguments(FastAPITestCase, FastAPICompatible): + broker_class = staticmethod(lambda: KafkaRouter().broker) + router_class = KafkaRouter + broker_wrapper = staticmethod(TestKafkaBroker) + + def build_app(self, router): + return router + + +class TestRouterPublisher(PublisherTestcase): + broker_class = staticmethod(lambda: KafkaRouter().broker) + + def build_app(self, router): + return router + + +def test_fastapi_security_schema() -> None: + security = SASLPlaintext(username="user", password="pass", use_ssl=False) + + router = KafkaRouter("localhost:9092", security=security) + + schema = AsyncAPI(router.broker, schema_version="2.6.0").to_jsonable() + + assert schema["servers"]["development"] == { + "protocol": "kafka", + "protocolVersion": "auto", + "security": [{"user-password": []}], + "url": "localhost:9092", + } + assert schema["components"]["securitySchemes"] == { + "user-password": {"type": "userPassword"}, + } diff --git a/tests/asyncapi/confluent/v2_6_0/test_naming.py b/tests/asyncapi/confluent/v2_6_0/test_naming.py new file mode 100644 index 0000000000..2fdbd64687 --- /dev/null +++ b/tests/asyncapi/confluent/v2_6_0/test_naming.py @@ -0,0 +1,51 @@ +from faststream.confluent import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.naming import NamingTestCase + + +class TestNaming(NamingTestCase): + broker_class = KafkaBroker + + def test_base(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "defaultContentType": "application/json", + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "servers": { + "development": { + "url": "localhost", + "protocol": "kafka", + "protocolVersion": "auto", + }, + }, + "channels": { + "test:Handle": { + "servers": ["development"], + "bindings": {"kafka": {"topic": "test", "bindingVersion": "0.4.0"}}, + "publish": { + "message": { + "$ref": "#/components/messages/test:Handle:Message" + }, + }, + }, + }, + "components": { + "messages": { + "test:Handle:Message": { + "title": "test:Handle:Message", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/EmptyPayload"}, + }, + }, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + }, + } diff --git a/tests/asyncapi/confluent/v2_6_0/test_publisher.py b/tests/asyncapi/confluent/v2_6_0/test_publisher.py new file mode 100644 index 0000000000..353774d562 --- /dev/null +++ b/tests/asyncapi/confluent/v2_6_0/test_publisher.py @@ -0,0 +1,20 @@ +from faststream.confluent import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase + + +class TestArguments(PublisherTestcase): + broker_class = KafkaBroker + + def test_publisher_bindings(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "kafka": {"bindingVersion": "0.4.0", "topic": "test"}, + } diff --git a/tests/asyncapi/confluent/v2_6_0/test_router.py b/tests/asyncapi/confluent/v2_6_0/test_router.py new file mode 100644 index 0000000000..c73885cddb --- /dev/null +++ b/tests/asyncapi/confluent/v2_6_0/test_router.py @@ -0,0 +1,84 @@ +from faststream.confluent import KafkaBroker, KafkaPublisher, KafkaRoute, KafkaRouter +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase +from tests.asyncapi.base.v2_6_0.router import RouterTestcase + + +class TestRouter(RouterTestcase): + broker_class = KafkaBroker + router_class = KafkaRouter + route_class = KafkaRoute + publisher_class = KafkaPublisher + + def test_prefix(self) -> None: + broker = self.broker_class() + + router = self.router_class(prefix="test_") + + @router.subscriber("test") + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "defaultContentType": "application/json", + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "servers": { + "development": { + "url": "localhost", + "protocol": "kafka", + "protocolVersion": "auto", + }, + }, + "channels": { + "test_test:Handle": { + "servers": ["development"], + "bindings": { + "kafka": {"topic": "test_test", "bindingVersion": "0.4.0"}, + }, + "publish": { + "message": { + "$ref": "#/components/messages/test_test:Handle:Message", + }, + }, + }, + }, + "components": { + "messages": { + "test_test:Handle:Message": { + "title": "test_test:Handle:Message", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": { + "$ref": "#/components/schemas/Handle:Message:Payload", + }, + }, + }, + "schemas": { + "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + }, + }, + } + + +class TestRouterArguments(ArgumentsTestcase): + broker_class = KafkaRouter + + def build_app(self, router): + broker = KafkaBroker() + broker.include_router(router) + return broker + + +class TestRouterPublisher(PublisherTestcase): + broker_class = KafkaRouter + + def build_app(self, router): + broker = KafkaBroker() + broker.include_router(router) + return broker diff --git a/tests/asyncapi/confluent/v2_6_0/test_security.py b/tests/asyncapi/confluent/v2_6_0/test_security.py new file mode 100644 index 0000000000..8a8fe9f685 --- /dev/null +++ b/tests/asyncapi/confluent/v2_6_0/test_security.py @@ -0,0 +1,7 @@ +from tests.asyncapi.confluent.security import SecurityTestcase + +from .base import AsyncAPI26Mixin + + +class TestSecurity(AsyncAPI26Mixin, SecurityTestcase): + pass diff --git a/tests/a_docs/getting_started/cli/confluent/__init__.py b/tests/asyncapi/confluent/v3_0_0/__init__.py similarity index 100% rename from tests/a_docs/getting_started/cli/confluent/__init__.py rename to tests/asyncapi/confluent/v3_0_0/__init__.py diff --git a/tests/asyncapi/confluent/v3_0_0/base.py b/tests/asyncapi/confluent/v3_0_0/base.py new file mode 100644 index 0000000000..6cbf268b02 --- /dev/null +++ b/tests/asyncapi/confluent/v3_0_0/base.py @@ -0,0 +1,8 @@ +from faststream.confluent import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from faststream.specification.base.specification import Specification + + +class AsyncAPI30Mixin: + def get_schema(self, broker: KafkaBroker) -> Specification: + return AsyncAPI(broker, schema_version="3.0.0") diff --git a/tests/asyncapi/confluent/v3_0_0/test_arguments.py b/tests/asyncapi/confluent/v3_0_0/test_arguments.py new file mode 100644 index 0000000000..99b88175e2 --- /dev/null +++ b/tests/asyncapi/confluent/v3_0_0/test_arguments.py @@ -0,0 +1,20 @@ +from faststream.confluent import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.arguments import ArgumentsTestcase + + +class TestArguments(ArgumentsTestcase): + broker_factory = KafkaBroker + + def test_subscriber_bindings(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "kafka": {"bindingVersion": "0.4.0", "topic": "test"}, + } diff --git a/tests/asyncapi/confluent/v3_0_0/test_connection.py b/tests/asyncapi/confluent/v3_0_0/test_connection.py new file mode 100644 index 0000000000..63b9c51da3 --- /dev/null +++ b/tests/asyncapi/confluent/v3_0_0/test_connection.py @@ -0,0 +1,98 @@ +from faststream.confluent import KafkaBroker +from faststream.specification import Tag +from faststream.specification.asyncapi import AsyncAPI + + +def test_base() -> None: + schema = AsyncAPI( + KafkaBroker( + "kafka:9092", + protocol="plaintext", + protocol_version="0.9.0", + description="Test description", + tags=(Tag(name="some-tag", description="experimental"),), + ), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "description": "Test description", + "protocol": "plaintext", + "protocolVersion": "0.9.0", + "tags": [{"description": "experimental", "name": "some-tag"}], + "host": "kafka:9092", + "pathname": "", + }, + }, + } + + +def test_multi() -> None: + schema = AsyncAPI( + KafkaBroker(["kafka:9092", "kafka:9093"]), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "kafka", + "protocolVersion": "auto", + "host": "kafka:9092", + "pathname": "", + }, + "Server2": { + "protocol": "kafka", + "protocolVersion": "auto", + "host": "kafka:9093", + "pathname": "", + }, + }, + } + + +def test_custom() -> None: + schema = AsyncAPI( + KafkaBroker( + ["kafka:9092", "kafka:9093"], + specification_url=["kafka:9094", "kafka:9095"], + ), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "kafka", + "protocolVersion": "auto", + "host": "kafka:9094", + "pathname": "", + }, + "Server2": { + "protocol": "kafka", + "protocolVersion": "auto", + "host": "kafka:9095", + "pathname": "", + }, + }, + } diff --git a/tests/asyncapi/confluent/v3_0_0/test_fastapi.py b/tests/asyncapi/confluent/v3_0_0/test_fastapi.py new file mode 100644 index 0000000000..48a4ae5dda --- /dev/null +++ b/tests/asyncapi/confluent/v3_0_0/test_fastapi.py @@ -0,0 +1,42 @@ +from faststream.confluent.fastapi import KafkaRouter +from faststream.confluent.testing import TestKafkaBroker +from faststream.security import SASLPlaintext +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.arguments import FastAPICompatible +from tests.asyncapi.base.v3_0_0.fastapi import FastAPITestCase +from tests.asyncapi.base.v3_0_0.publisher import PublisherTestcase + + +class TestRouterArguments(FastAPITestCase, FastAPICompatible): + broker_factory = staticmethod(lambda: KafkaRouter().broker) + router_factory = KafkaRouter + broker_wrapper = staticmethod(TestKafkaBroker) + + def build_app(self, router): + return router + + +class TestRouterPublisher(PublisherTestcase): + broker_factory = staticmethod(lambda: KafkaRouter().broker) + + def build_app(self, router): + return router + + +def test_fastapi_security_schema() -> None: + security = SASLPlaintext(username="user", password="pass", use_ssl=False) + + router = KafkaRouter("localhost:9092", security=security) + + schema = AsyncAPI(router.broker, schema_version="3.0.0").to_jsonable() + + assert schema["servers"]["development"] == { + "protocol": "kafka", + "protocolVersion": "auto", + "security": [{"user-password": []}], + "host": "localhost:9092", + "pathname": "", + } + assert schema["components"]["securitySchemes"] == { + "user-password": {"type": "userPassword"}, + } diff --git a/tests/asyncapi/confluent/v3_0_0/test_naming.py b/tests/asyncapi/confluent/v3_0_0/test_naming.py new file mode 100644 index 0000000000..54ee7f2703 --- /dev/null +++ b/tests/asyncapi/confluent/v3_0_0/test_naming.py @@ -0,0 +1,70 @@ +from faststream.confluent import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.naming import NamingTestCase + + +class TestNaming(NamingTestCase): + broker_class = KafkaBroker + + def test_base(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "defaultContentType": "application/json", + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "servers": { + "development": { + "host": "localhost", + "pathname": "", + "protocol": "kafka", + "protocolVersion": "auto", + }, + }, + "channels": { + "test:Handle": { + "address": "test:Handle", + "servers": [ + { + "$ref": "#/servers/development", + }, + ], + "bindings": {"kafka": {"topic": "test", "bindingVersion": "0.4.0"}}, + "messages": { + "SubscribeMessage": { + "$ref": "#/components/messages/test:Handle:SubscribeMessage", + }, + }, + }, + }, + "operations": { + "test:HandleSubscribe": { + "action": "receive", + "channel": { + "$ref": "#/channels/test:Handle", + }, + "messages": [ + { + "$ref": "#/channels/test:Handle/messages/SubscribeMessage", + }, + ], + }, + }, + "components": { + "messages": { + "test:Handle:SubscribeMessage": { + "title": "test:Handle:SubscribeMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/EmptyPayload"}, + }, + }, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + }, + } diff --git a/tests/asyncapi/confluent/v3_0_0/test_publisher.py b/tests/asyncapi/confluent/v3_0_0/test_publisher.py new file mode 100644 index 0000000000..d6707c186d --- /dev/null +++ b/tests/asyncapi/confluent/v3_0_0/test_publisher.py @@ -0,0 +1,20 @@ +from faststream.confluent import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.publisher import PublisherTestcase + + +class TestArguments(PublisherTestcase): + broker_factory = KafkaBroker + + def test_publisher_bindings(self) -> None: + broker = self.broker_factory() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "kafka": {"bindingVersion": "0.4.0", "topic": "test"}, + } diff --git a/tests/asyncapi/confluent/v3_0_0/test_router.py b/tests/asyncapi/confluent/v3_0_0/test_router.py new file mode 100644 index 0000000000..0cb1cf9bcd --- /dev/null +++ b/tests/asyncapi/confluent/v3_0_0/test_router.py @@ -0,0 +1,97 @@ +from faststream.confluent import KafkaBroker, KafkaPublisher, KafkaRoute, KafkaRouter +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase +from tests.asyncapi.base.v3_0_0.router import RouterTestcase + + +class TestRouter(RouterTestcase): + broker_class = KafkaBroker + router_class = KafkaRouter + route_class = KafkaRoute + publisher_class = KafkaPublisher + + def test_prefix(self) -> None: + broker = self.broker_class() + + router = self.router_class(prefix="test_") + + @router.subscriber("test") + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert schema == { + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "asyncapi": "3.0.0", + "defaultContentType": "application/json", + "servers": { + "development": { + "host": "localhost", + "pathname": "", + "protocol": "kafka", + "protocolVersion": "auto", + }, + }, + "channels": { + "test_test:Handle": { + "address": "test_test:Handle", + "servers": [{"$ref": "#/servers/development"}], + "messages": { + "SubscribeMessage": { + "$ref": "#/components/messages/test_test:Handle:SubscribeMessage", + }, + }, + "bindings": { + "kafka": {"topic": "test_test", "bindingVersion": "0.4.0"}, + }, + }, + }, + "operations": { + "test_test:HandleSubscribe": { + "action": "receive", + "messages": [ + { + "$ref": "#/channels/test_test:Handle/messages/SubscribeMessage", + }, + ], + "channel": {"$ref": "#/channels/test_test:Handle"}, + }, + }, + "components": { + "messages": { + "test_test:Handle:SubscribeMessage": { + "title": "test_test:Handle:SubscribeMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": { + "$ref": "#/components/schemas/Handle:Message:Payload", + }, + }, + }, + "schemas": { + "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + }, + }, + } + + +class TestRouterArguments(ArgumentsTestcase): + broker_class = KafkaRouter + + def build_app(self, router): + broker = KafkaBroker() + broker.include_router(router) + return broker + + +class TestRouterPublisher(PublisherTestcase): + broker_class = KafkaRouter + + def build_app(self, router): + broker = KafkaBroker() + broker.include_router(router) + return broker diff --git a/tests/asyncapi/confluent/v3_0_0/test_security.py b/tests/asyncapi/confluent/v3_0_0/test_security.py new file mode 100644 index 0000000000..adb5c6d7f4 --- /dev/null +++ b/tests/asyncapi/confluent/v3_0_0/test_security.py @@ -0,0 +1,7 @@ +from tests.asyncapi.confluent.security import SecurityTestcase + +from .base import AsyncAPI30Mixin + + +class TestSecurity(AsyncAPI30Mixin, SecurityTestcase): + pass diff --git a/tests/asyncapi/kafka/test_app.py b/tests/asyncapi/kafka/test_app.py deleted file mode 100644 index 5417db30b4..0000000000 --- a/tests/asyncapi/kafka/test_app.py +++ /dev/null @@ -1,217 +0,0 @@ -from dirty_equals import IsPartialDict - -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.asyncapi.schema import Contact, ExternalDocs, License, Tag -from faststream.kafka import KafkaBroker - - -def test_base(): - schema = get_app_schema(FastStream(KafkaBroker())).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "localhost", - } - }, - } - - -def test_with_name(): - schema = get_app_schema( - FastStream( - KafkaBroker(), - title="My App", - version="1.0.0", - description="Test description", - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": { - "description": "Test description", - "title": "My App", - "version": "1.0.0", - }, - "servers": { - "development": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "localhost", - } - }, - } - - -def test_full(): - schema = get_app_schema( - FastStream( - KafkaBroker(), - title="My App", - version="1.0.0", - description="Test description", - license=License(name="MIT", url="https://mit.com/"), - terms_of_service="https://my-terms.com/", - contact=Contact(name="support", url="https://help.com/"), - tags=(Tag(name="some-tag", description="experimental"),), - identifier="some-unique-uuid", - external_docs=ExternalDocs( - url="https://extra-docs.py/", - ), - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "externalDocs": {"url": "https://extra-docs.py/"}, - "id": "some-unique-uuid", - "info": { - "contact": {"name": "support", "url": "https://help.com/"}, - "description": "Test description", - "license": {"name": "MIT", "url": "https://mit.com/"}, - "termsOfService": "https://my-terms.com/", - "title": "My App", - "version": "1.0.0", - }, - "servers": { - "development": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "localhost", - } - }, - "tags": [{"description": "experimental", "name": "some-tag"}], - } - - -def test_full_dict(): - schema = get_app_schema( - FastStream( - KafkaBroker(), - title="My App", - version="1.0.0", - description="Test description", - license={"name": "MIT", "url": "https://mit.com/"}, - terms_of_service="https://my-terms.com/", - contact={"name": "support", "url": "https://help.com/"}, - tags=({"name": "some-tag", "description": "experimental"},), - identifier="some-unique-uuid", - external_docs={ - "url": "https://extra-docs.py/", - }, - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "externalDocs": {"url": "https://extra-docs.py/"}, - "id": "some-unique-uuid", - "info": { - "contact": {"name": "support", "url": "https://help.com/"}, - "description": "Test description", - "license": {"name": "MIT", "url": "https://mit.com/"}, - "termsOfService": "https://my-terms.com/", - "title": "My App", - "version": "1.0.0", - }, - "servers": { - "development": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "localhost", - } - }, - "tags": [{"description": "experimental", "name": "some-tag"}], - } - - -def test_extra(): - schema = get_app_schema( - FastStream( - KafkaBroker(), - title="My App", - version="1.0.0", - description="Test description", - license={ - "name": "MIT", - "url": "https://mit.com/", - "x-field": "extra", - }, - terms_of_service="https://my-terms.com/", - contact={ - "name": "support", - "url": "https://help.com/", - "x-field": "extra", - }, - tags=( - { - "name": "some-tag", - "description": "experimental", - "x-field": "extra", - }, - ), - identifier="some-unique-uuid", - external_docs={ - "url": "https://extra-docs.py/", - "x-field": "extra", - }, - ) - ).to_jsonable() - - assert schema == IsPartialDict( - { - "externalDocs": { - "url": "https://extra-docs.py/", - "x-field": "extra", - }, - "id": "some-unique-uuid", - "info": { - "contact": { - "name": "support", - "url": "https://help.com/", - "x-field": "extra", - }, - "description": "Test description", - "license": { - "name": "MIT", - "url": "https://mit.com/", - "x-field": "extra", - }, - "termsOfService": "https://my-terms.com/", - "title": "My App", - "version": "1.0.0", - }, - "servers": { - "development": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "localhost", - } - }, - "tags": [ - { - "description": "experimental", - "name": "some-tag", - "x-field": "extra", - } - ], - } - ) diff --git a/tests/asyncapi/kafka/test_arguments.py b/tests/asyncapi/kafka/test_arguments.py deleted file mode 100644 index 5b289ffd97..0000000000 --- a/tests/asyncapi/kafka/test_arguments.py +++ /dev/null @@ -1,58 +0,0 @@ -from aiokafka import TopicPartition - -from faststream.asyncapi.generate import get_app_schema -from faststream.kafka import KafkaBroker -from tests.asyncapi.base.arguments import ArgumentsTestcase - - -class TestArguments(ArgumentsTestcase): - broker_class = KafkaBroker - - def test_subscriber_bindings(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "kafka": {"bindingVersion": "0.4.0", "topic": "test"} - } - - def test_subscriber_with_one_topic_partitions(self): - broker = self.broker_class() - - part1 = TopicPartition("topic_name", 1) - part2 = TopicPartition("topic_name", 2) - - @broker.subscriber(partitions=[part1, part2]) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "kafka": {"bindingVersion": "0.4.0", "topic": "topic_name"} - } - - def test_subscriber_with_multi_topics_partitions(self): - broker = self.broker_class() - - part1 = TopicPartition("topic_name1", 1) - part2 = TopicPartition("topic_name2", 2) - - @broker.subscriber(partitions=[part1, part2]) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key1 = tuple(schema["channels"].keys())[0] # noqa: RUF015 - key2 = tuple(schema["channels"].keys())[1] - - assert sorted( - ( - schema["channels"][key1]["bindings"]["kafka"]["topic"], - schema["channels"][key2]["bindings"]["kafka"]["topic"], - ) - ) == sorted(("topic_name1", "topic_name2")) diff --git a/tests/asyncapi/kafka/test_connection.py b/tests/asyncapi/kafka/test_connection.py deleted file mode 100644 index 25eb392361..0000000000 --- a/tests/asyncapi/kafka/test_connection.py +++ /dev/null @@ -1,92 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.asyncapi.schema import Tag -from faststream.kafka import KafkaBroker - - -def test_base(): - schema = get_app_schema( - FastStream( - KafkaBroker( - "kafka:9092", - protocol="plaintext", - protocol_version="0.9.0", - description="Test description", - tags=(Tag(name="some-tag", description="experimental"),), - ) - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "description": "Test description", - "protocol": "plaintext", - "protocolVersion": "0.9.0", - "tags": [{"description": "experimental", "name": "some-tag"}], - "url": "kafka:9092", - } - }, - } - - -def test_multi(): - schema = get_app_schema( - FastStream(KafkaBroker(["kafka:9092", "kafka:9093"])) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "Server1": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "kafka:9092", - }, - "Server2": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "kafka:9093", - }, - }, - } - - -def test_custom(): - schema = get_app_schema( - FastStream( - KafkaBroker( - ["kafka:9092", "kafka:9093"], - asyncapi_url=["kafka:9094", "kafka:9095"], - ) - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "Server1": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "kafka:9094", - }, - "Server2": { - "protocol": "kafka", - "protocolVersion": "auto", - "url": "kafka:9095", - }, - }, - } diff --git a/tests/asyncapi/kafka/test_fastapi.py b/tests/asyncapi/kafka/test_fastapi.py deleted file mode 100644 index 0991c3586d..0000000000 --- a/tests/asyncapi/kafka/test_fastapi.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Type - -from faststream.asyncapi.generate import get_app_schema -from faststream.kafka.fastapi import KafkaRouter -from faststream.kafka.testing import TestKafkaBroker -from faststream.security import SASLPlaintext -from tests.asyncapi.base.arguments import FastAPICompatible -from tests.asyncapi.base.fastapi import FastAPITestCase -from tests.asyncapi.base.publisher import PublisherTestcase - - -class TestRouterArguments(FastAPITestCase, FastAPICompatible): - broker_class: Type[KafkaRouter] = KafkaRouter - broker_wrapper = staticmethod(TestKafkaBroker) - - def build_app(self, router): - return router - - -class TestRouterPublisher(PublisherTestcase): - broker_class = KafkaRouter - - def build_app(self, router): - return router - - -def test_fastapi_security_schema(): - security = SASLPlaintext(username="user", password="pass", use_ssl=False) - - broker = KafkaRouter("localhost:9092", security=security) - - schema = get_app_schema(broker).to_jsonable() - - assert schema["servers"]["development"] == { - "protocol": "kafka", - "protocolVersion": "auto", - "security": [{"user-password": []}], - "url": "localhost:9092", - } - assert schema["components"]["securitySchemes"] == { - "user-password": {"type": "userPassword"} - } diff --git a/tests/asyncapi/kafka/test_naming.py b/tests/asyncapi/kafka/test_naming.py deleted file mode 100644 index ed8f18bd98..0000000000 --- a/tests/asyncapi/kafka/test_naming.py +++ /dev/null @@ -1,50 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.kafka import KafkaBroker -from tests.asyncapi.base.naming import NamingTestCase - - -class TestNaming(NamingTestCase): - broker_class = KafkaBroker - - def test_base(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "defaultContentType": "application/json", - "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, - "servers": { - "development": { - "url": "localhost", - "protocol": "kafka", - "protocolVersion": "auto", - } - }, - "channels": { - "test:Handle": { - "servers": ["development"], - "bindings": {"kafka": {"topic": "test", "bindingVersion": "0.4.0"}}, - "subscribe": { - "message": {"$ref": "#/components/messages/test:Handle:Message"} - }, - } - }, - "components": { - "messages": { - "test:Handle:Message": { - "title": "test:Handle:Message", - "correlationId": { - "location": "$message.header#/correlation_id" - }, - "payload": {"$ref": "#/components/schemas/EmptyPayload"}, - } - }, - "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, - }, - } diff --git a/tests/asyncapi/kafka/test_publisher.py b/tests/asyncapi/kafka/test_publisher.py deleted file mode 100644 index 0b90bd6f4f..0000000000 --- a/tests/asyncapi/kafka/test_publisher.py +++ /dev/null @@ -1,20 +0,0 @@ -from faststream.asyncapi.generate import get_app_schema -from faststream.kafka import KafkaBroker -from tests.asyncapi.base.publisher import PublisherTestcase - - -class TestArguments(PublisherTestcase): - broker_class = KafkaBroker - - def test_publisher_bindings(self): - broker = self.broker_class() - - @broker.publisher("test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "kafka": {"bindingVersion": "0.4.0", "topic": "test"} - } diff --git a/tests/asyncapi/kafka/test_router.py b/tests/asyncapi/kafka/test_router.py deleted file mode 100644 index 5cb2cc8168..0000000000 --- a/tests/asyncapi/kafka/test_router.py +++ /dev/null @@ -1,85 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.kafka import KafkaBroker, KafkaPublisher, KafkaRoute, KafkaRouter -from tests.asyncapi.base.arguments import ArgumentsTestcase -from tests.asyncapi.base.publisher import PublisherTestcase -from tests.asyncapi.base.router import RouterTestcase - - -class TestRouter(RouterTestcase): - broker_class = KafkaBroker - router_class = KafkaRouter - route_class = KafkaRoute - publisher_class = KafkaPublisher - - def test_prefix(self): - broker = self.broker_class() - - router = self.router_class(prefix="test_") - - @router.subscriber("test") - async def handle(msg): ... - - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "defaultContentType": "application/json", - "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, - "servers": { - "development": { - "url": "localhost", - "protocol": "kafka", - "protocolVersion": "auto", - } - }, - "channels": { - "test_test:Handle": { - "servers": ["development"], - "bindings": { - "kafka": {"topic": "test_test", "bindingVersion": "0.4.0"} - }, - "subscribe": { - "message": { - "$ref": "#/components/messages/test_test:Handle:Message" - } - }, - } - }, - "components": { - "messages": { - "test_test:Handle:Message": { - "title": "test_test:Handle:Message", - "correlationId": { - "location": "$message.header#/correlation_id" - }, - "payload": { - "$ref": "#/components/schemas/Handle:Message:Payload" - }, - } - }, - "schemas": { - "Handle:Message:Payload": {"title": "Handle:Message:Payload"} - }, - }, - } - - -class TestRouterArguments(ArgumentsTestcase): - broker_class = KafkaRouter - - def build_app(self, router): - broker = KafkaBroker() - broker.include_router(router) - return FastStream(broker) - - -class TestRouterPublisher(PublisherTestcase): - broker_class = KafkaRouter - - def build_app(self, router): - broker = KafkaBroker() - broker.include_router(router) - return FastStream(broker) diff --git a/tests/asyncapi/kafka/test_security.py b/tests/asyncapi/kafka/test_security.py deleted file mode 100644 index 62e30e9ccf..0000000000 --- a/tests/asyncapi/kafka/test_security.py +++ /dev/null @@ -1,223 +0,0 @@ -import ssl -from copy import deepcopy - -from faststream.app import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.kafka import KafkaBroker -from faststream.security import ( - SASLGSSAPI, - BaseSecurity, - SASLOAuthBearer, - SASLPlaintext, - SASLScram256, - SASLScram512, -) - -basic_schema = { - "asyncapi": "2.6.0", - "channels": { - "test_1:TestTopic": { - "bindings": {"kafka": {"bindingVersion": "0.4.0", "topic": "test_1"}}, - "servers": ["development"], - "subscribe": { - "message": {"$ref": "#/components/messages/test_1:TestTopic:Message"} - }, - }, - "test_2:Publisher": { - "bindings": {"kafka": {"bindingVersion": "0.4.0", "topic": "test_2"}}, - "publish": { - "message": {"$ref": "#/components/messages/test_2:Publisher:Message"} - }, - "servers": ["development"], - }, - }, - "components": { - "messages": { - "test_1:TestTopic:Message": { - "correlationId": {"location": "$message.header#/correlation_id"}, - "payload": {"$ref": "#/components/schemas/TestTopic:Message:Payload"}, - "title": "test_1:TestTopic:Message", - }, - "test_2:Publisher:Message": { - "correlationId": {"location": "$message.header#/correlation_id"}, - "payload": { - "$ref": "#/components/schemas/test_2:Publisher:Message:Payload" - }, - "title": "test_2:Publisher:Message", - }, - }, - "schemas": { - "TestTopic:Message:Payload": { - "title": "TestTopic:Message:Payload", - "type": "string", - }, - "test_2:Publisher:Message:Payload": { - "title": "test_2:Publisher:Message:Payload", - "type": "string", - }, - }, - "securitySchemes": {}, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "kafka-secure", - "protocolVersion": "auto", - "security": [], - "url": "localhost:9092", - } - }, -} - - -def test_base_security_schema(): - ssl_context = ssl.create_default_context() - security = BaseSecurity(ssl_context=ssl_context) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - assert schema == basic_schema - - -def test_plaintext_security_schema(): - ssl_context = ssl.create_default_context() - security = SASLPlaintext( - ssl_context=ssl_context, - username="admin", - password="password", # pragma: allowlist secret - ) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - plaintext_security_schema = deepcopy(basic_schema) - plaintext_security_schema["servers"]["development"]["security"] = [ - {"user-password": []} - ] - plaintext_security_schema["components"]["securitySchemes"] = { - "user-password": {"type": "userPassword"} - } - - assert schema == plaintext_security_schema - - -def test_scram256_security_schema(): - ssl_context = ssl.create_default_context() - security = SASLScram256( - ssl_context=ssl_context, - username="admin", - password="password", # pragma: allowlist secret - ) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - sasl256_security_schema = deepcopy(basic_schema) - sasl256_security_schema["servers"]["development"]["security"] = [{"scram256": []}] - sasl256_security_schema["components"]["securitySchemes"] = { - "scram256": {"type": "scramSha256"} - } - - assert schema == sasl256_security_schema - - -def test_scram512_security_schema(): - ssl_context = ssl.create_default_context() - security = SASLScram512( - ssl_context=ssl_context, - username="admin", - password="password", # pragma: allowlist secret - ) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - sasl512_security_schema = deepcopy(basic_schema) - sasl512_security_schema["servers"]["development"]["security"] = [{"scram512": []}] - sasl512_security_schema["components"]["securitySchemes"] = { - "scram512": {"type": "scramSha512"} - } - - assert schema == sasl512_security_schema - - -def test_oauthbearer_security_schema(): - ssl_context = ssl.create_default_context() - security = SASLOAuthBearer( - ssl_context=ssl_context, - ) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - sasl_oauthbearer_security_schema = deepcopy(basic_schema) - sasl_oauthbearer_security_schema["servers"]["development"]["security"] = [ - {"oauthbearer": []} - ] - sasl_oauthbearer_security_schema["components"]["securitySchemes"] = { - "oauthbearer": {"type": "oauth2", "$ref": ""} - } - - assert schema == sasl_oauthbearer_security_schema - - -def test_gssapi_security_schema(): - ssl_context = ssl.create_default_context() - security = SASLGSSAPI( - ssl_context=ssl_context, - ) - - broker = KafkaBroker("localhost:9092", security=security) - app = FastStream(broker) - - @broker.publisher("test_2") - @broker.subscriber("test_1") - async def test_topic(msg: str) -> str: - pass - - schema = get_app_schema(app).to_jsonable() - - gssapi_security_schema = deepcopy(basic_schema) - gssapi_security_schema["servers"]["development"]["security"] = [{"gssapi": []}] - gssapi_security_schema["components"]["securitySchemes"] = { - "gssapi": {"type": "gssapi"} - } - - assert schema == gssapi_security_schema diff --git a/tests/a_docs/getting_started/asyncapi/asyncapi_customization/__init__.py b/tests/asyncapi/kafka/v2_6_0/__init__.py similarity index 100% rename from tests/a_docs/getting_started/asyncapi/asyncapi_customization/__init__.py rename to tests/asyncapi/kafka/v2_6_0/__init__.py diff --git a/tests/asyncapi/kafka/v2_6_0/test_app.py b/tests/asyncapi/kafka/v2_6_0/test_app.py new file mode 100644 index 0000000000..82cbe9c283 --- /dev/null +++ b/tests/asyncapi/kafka/v2_6_0/test_app.py @@ -0,0 +1,179 @@ +from dirty_equals import IsPartialDict + +from faststream.kafka import KafkaBroker +from faststream.specification import Contact, ExternalDocs, License, Tag +from faststream.specification.asyncapi import AsyncAPI + + +def test_base() -> None: + schema = AsyncAPI(KafkaBroker(), schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "localhost", + }, + }, + } + + +def test_with_name() -> None: + schema = AsyncAPI( + KafkaBroker(), + title="My App", + app_version="1.0.0", + description="Test description", + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": { + "description": "Test description", + "title": "My App", + "version": "1.0.0", + }, + "servers": { + "development": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "localhost", + }, + }, + } + + +def test_full() -> None: + schema = AsyncAPI( + KafkaBroker(), + title="My App", + app_version="1.0.0", + description="Test description", + license=License(name="MIT", url="https://mit.com/"), + terms_of_service="https://my-terms.com/", + contact=Contact(name="support", url="https://help.com/"), + tags=(Tag(name="some-tag", description="experimental"),), + identifier="some-unique-uuid", + external_docs=ExternalDocs( + url="https://extra-docs.py/", + ), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "externalDocs": {"url": "https://extra-docs.py/"}, + "id": "some-unique-uuid", + "info": { + "contact": {"name": "support", "url": "https://help.com/"}, + "description": "Test description", + "license": {"name": "MIT", "url": "https://mit.com/"}, + "termsOfService": "https://my-terms.com/", + "title": "My App", + "version": "1.0.0", + }, + "servers": { + "development": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "localhost", + }, + }, + "tags": [{"description": "experimental", "name": "some-tag"}], + } + + +def test_full_dict() -> None: + schema = AsyncAPI( + KafkaBroker(), + title="My App", + app_version="1.0.0", + description="Test description", + license={"name": "MIT", "url": "https://mit.com/"}, + terms_of_service="https://my-terms.com/", + contact={"name": "support", "url": "https://help.com/"}, + tags=({"name": "some-tag", "description": "experimental"},), + identifier="some-unique-uuid", + external_docs={ + "url": "https://extra-docs.py/", + }, + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "externalDocs": {"url": "https://extra-docs.py/"}, + "id": "some-unique-uuid", + "info": { + "contact": {"name": "support", "url": "https://help.com/"}, + "description": "Test description", + "license": {"name": "MIT", "url": "https://mit.com/"}, + "termsOfService": "https://my-terms.com/", + "title": "My App", + "version": "1.0.0", + }, + "servers": { + "development": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "localhost", + }, + }, + "tags": [{"description": "experimental", "name": "some-tag"}], + } + + +def test_extra() -> None: + schema = AsyncAPI( + KafkaBroker(), + title="My App", + app_version="1.0.0", + description="Test description", + license={"name": "MIT", "url": "https://mit.com/", "x-field": "extra"}, + terms_of_service="https://my-terms.com/", + contact={"name": "support", "url": "https://help.com/", "x-field": "extra"}, + tags=({"name": "some-tag", "description": "experimental", "x-field": "extra"},), + identifier="some-unique-uuid", + external_docs={ + "url": "https://extra-docs.py/", + "x-field": "extra", + }, + schema_version="2.6.0", + ).to_jsonable() + + assert schema == IsPartialDict({ + "info": { + "title": "My App", + "version": "1.0.0", + "description": "Test description", + "termsOfService": "https://my-terms.com/", + "contact": { + "name": "support", + "url": "https://help.com/", + "x-field": "extra", + }, + "license": {"name": "MIT", "url": "https://mit.com/", "x-field": "extra"}, + }, + "asyncapi": "2.6.0", + "id": "some-unique-uuid", + "tags": [ + {"name": "some-tag", "description": "experimental", "x-field": "extra"} + ], + "externalDocs": {"url": "https://extra-docs.py/", "x-field": "extra"}, + }) diff --git a/tests/asyncapi/kafka/v2_6_0/test_arguments.py b/tests/asyncapi/kafka/v2_6_0/test_arguments.py new file mode 100644 index 0000000000..c6af2e02ac --- /dev/null +++ b/tests/asyncapi/kafka/v2_6_0/test_arguments.py @@ -0,0 +1,54 @@ +from faststream.kafka import KafkaBroker, TopicPartition +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase + + +class TestArguments(ArgumentsTestcase): + broker_class = KafkaBroker + + def test_subscriber_bindings(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "kafka": {"bindingVersion": "0.4.0", "topic": "test"}, + } + + def test_subscriber_with_one_topic_partitions(self) -> None: + broker = self.broker_class() + + part1 = TopicPartition("topic_name", 1) + part2 = TopicPartition("topic_name", 2) + + @broker.subscriber(partitions=[part1, part2]) + async def handle(msg): ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "kafka": {"bindingVersion": "0.4.0", "topic": "topic_name"} + } + + def test_subscriber_with_multi_topics_partitions(self) -> None: + broker = self.broker_class() + + part1 = TopicPartition("topic_name1", 1) + part2 = TopicPartition("topic_name2", 2) + + @broker.subscriber(partitions=[part1, part2]) + async def handle(msg): ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key1 = tuple(schema["channels"].keys())[0] # noqa: RUF015 + key2 = tuple(schema["channels"].keys())[1] + + assert sorted(( + schema["channels"][key1]["bindings"]["kafka"]["topic"], + schema["channels"][key2]["bindings"]["kafka"]["topic"], + )) == sorted(("topic_name1", "topic_name2")) diff --git a/tests/asyncapi/kafka/v2_6_0/test_connection.py b/tests/asyncapi/kafka/v2_6_0/test_connection.py new file mode 100644 index 0000000000..2107e3882b --- /dev/null +++ b/tests/asyncapi/kafka/v2_6_0/test_connection.py @@ -0,0 +1,90 @@ +from faststream.kafka import KafkaBroker +from faststream.specification import Tag +from faststream.specification.asyncapi import AsyncAPI + + +def test_base() -> None: + schema = AsyncAPI( + KafkaBroker( + "kafka:9092", + protocol="plaintext", + protocol_version="0.9.0", + description="Test description", + tags=(Tag(name="some-tag", description="experimental"),), + ), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "description": "Test description", + "protocol": "plaintext", + "protocolVersion": "0.9.0", + "tags": [{"description": "experimental", "name": "some-tag"}], + "url": "kafka:9092", + }, + }, + } + + +def test_multi() -> None: + schema = AsyncAPI( + KafkaBroker(["kafka:9092", "kafka:9093"]), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "kafka:9092", + }, + "Server2": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "kafka:9093", + }, + }, + } + + +def test_custom() -> None: + schema = AsyncAPI( + KafkaBroker( + ["kafka:9092", "kafka:9093"], + specification_url=["kafka:9094", "kafka:9095"], + ), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "kafka:9094", + }, + "Server2": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "kafka:9095", + }, + }, + } diff --git a/tests/asyncapi/kafka/v2_6_0/test_fastapi.py b/tests/asyncapi/kafka/v2_6_0/test_fastapi.py new file mode 100644 index 0000000000..7e55fa1db3 --- /dev/null +++ b/tests/asyncapi/kafka/v2_6_0/test_fastapi.py @@ -0,0 +1,41 @@ +from faststream.kafka.fastapi import KafkaRouter +from faststream.kafka.testing import TestKafkaBroker +from faststream.security import SASLPlaintext +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import FastAPICompatible +from tests.asyncapi.base.v2_6_0.fastapi import FastAPITestCase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase + + +class TestRouterArguments(FastAPITestCase, FastAPICompatible): + broker_class = staticmethod(lambda: KafkaRouter().broker) + router_class = KafkaRouter + broker_wrapper = staticmethod(TestKafkaBroker) + + def build_app(self, router): + return router + + +class TestRouterPublisher(PublisherTestcase): + broker_class = staticmethod(lambda: KafkaRouter().broker) + + def build_app(self, router): + return router + + +def test_fastapi_security_schema() -> None: + security = SASLPlaintext(username="user", password="pass", use_ssl=False) + + router = KafkaRouter("localhost:9092", security=security) + + schema = AsyncAPI(router.broker, schema_version="2.6.0").to_jsonable() + + assert schema["servers"]["development"] == { + "protocol": "kafka", + "protocolVersion": "auto", + "security": [{"user-password": []}], + "url": "localhost:9092", + } + assert schema["components"]["securitySchemes"] == { + "user-password": {"type": "userPassword"}, + } diff --git a/tests/asyncapi/kafka/v2_6_0/test_naming.py b/tests/asyncapi/kafka/v2_6_0/test_naming.py new file mode 100644 index 0000000000..bba38e11b7 --- /dev/null +++ b/tests/asyncapi/kafka/v2_6_0/test_naming.py @@ -0,0 +1,51 @@ +from faststream.kafka import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.naming import NamingTestCase + + +class TestNaming(NamingTestCase): + broker_class = KafkaBroker + + def test_base(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "defaultContentType": "application/json", + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "servers": { + "development": { + "url": "localhost", + "protocol": "kafka", + "protocolVersion": "auto", + }, + }, + "channels": { + "test:Handle": { + "servers": ["development"], + "bindings": {"kafka": {"topic": "test", "bindingVersion": "0.4.0"}}, + "publish": { + "message": { + "$ref": "#/components/messages/test:Handle:Message" + }, + }, + }, + }, + "components": { + "messages": { + "test:Handle:Message": { + "title": "test:Handle:Message", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/EmptyPayload"}, + }, + }, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + }, + } diff --git a/tests/asyncapi/kafka/v2_6_0/test_publisher.py b/tests/asyncapi/kafka/v2_6_0/test_publisher.py new file mode 100644 index 0000000000..da3098352a --- /dev/null +++ b/tests/asyncapi/kafka/v2_6_0/test_publisher.py @@ -0,0 +1,20 @@ +from faststream.kafka import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase + + +class TestArguments(PublisherTestcase): + broker_class = KafkaBroker + + def test_publisher_bindings(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "kafka": {"bindingVersion": "0.4.0", "topic": "test"}, + } diff --git a/tests/asyncapi/kafka/v2_6_0/test_router.py b/tests/asyncapi/kafka/v2_6_0/test_router.py new file mode 100644 index 0000000000..2fd0342eb0 --- /dev/null +++ b/tests/asyncapi/kafka/v2_6_0/test_router.py @@ -0,0 +1,84 @@ +from faststream.kafka import KafkaBroker, KafkaPublisher, KafkaRoute, KafkaRouter +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase +from tests.asyncapi.base.v2_6_0.router import RouterTestcase + + +class TestRouter(RouterTestcase): + broker_class = KafkaBroker + router_class = KafkaRouter + route_class = KafkaRoute + publisher_class = KafkaPublisher + + def test_prefix(self) -> None: + broker = self.broker_class() + + router = self.router_class(prefix="test_") + + @router.subscriber("test") + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "defaultContentType": "application/json", + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "servers": { + "development": { + "url": "localhost", + "protocol": "kafka", + "protocolVersion": "auto", + }, + }, + "channels": { + "test_test:Handle": { + "servers": ["development"], + "bindings": { + "kafka": {"topic": "test_test", "bindingVersion": "0.4.0"}, + }, + "publish": { + "message": { + "$ref": "#/components/messages/test_test:Handle:Message", + }, + }, + }, + }, + "components": { + "messages": { + "test_test:Handle:Message": { + "title": "test_test:Handle:Message", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": { + "$ref": "#/components/schemas/Handle:Message:Payload", + }, + }, + }, + "schemas": { + "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + }, + }, + } + + +class TestRouterArguments(ArgumentsTestcase): + broker_class = KafkaRouter + + def build_app(self, router): + broker = KafkaBroker() + broker.include_router(router) + return broker + + +class TestRouterPublisher(PublisherTestcase): + broker_class = KafkaRouter + + def build_app(self, router): + broker = KafkaBroker() + broker.include_router(router) + return broker diff --git a/tests/asyncapi/kafka/v2_6_0/test_security.py b/tests/asyncapi/kafka/v2_6_0/test_security.py new file mode 100644 index 0000000000..b1275e242d --- /dev/null +++ b/tests/asyncapi/kafka/v2_6_0/test_security.py @@ -0,0 +1,216 @@ +import ssl +from copy import deepcopy + +from faststream.kafka import KafkaBroker +from faststream.security import ( + SASLGSSAPI, + BaseSecurity, + SASLOAuthBearer, + SASLPlaintext, + SASLScram256, + SASLScram512, +) +from faststream.specification.asyncapi import AsyncAPI + +basic_schema = { + "asyncapi": "2.6.0", + "channels": { + "test_1:TestTopic": { + "bindings": {"kafka": {"bindingVersion": "0.4.0", "topic": "test_1"}}, + "servers": ["development"], + "publish": { + "message": {"$ref": "#/components/messages/test_1:TestTopic:Message"}, + }, + }, + "test_2:Publisher": { + "bindings": {"kafka": {"bindingVersion": "0.4.0", "topic": "test_2"}}, + "subscribe": { + "message": {"$ref": "#/components/messages/test_2:Publisher:Message"}, + }, + "servers": ["development"], + }, + }, + "components": { + "messages": { + "test_1:TestTopic:Message": { + "correlationId": {"location": "$message.header#/correlation_id"}, + "payload": {"$ref": "#/components/schemas/TestTopic:Message:Payload"}, + "title": "test_1:TestTopic:Message", + }, + "test_2:Publisher:Message": { + "correlationId": {"location": "$message.header#/correlation_id"}, + "payload": { + "$ref": "#/components/schemas/test_2:Publisher:Message:Payload", + }, + "title": "test_2:Publisher:Message", + }, + }, + "schemas": { + "TestTopic:Message:Payload": { + "title": "TestTopic:Message:Payload", + "type": "string", + }, + "test_2:Publisher:Message:Payload": { + "title": "test_2:Publisher:Message:Payload", + "type": "string", + }, + }, + "securitySchemes": {}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "kafka-secure", + "protocolVersion": "auto", + "security": [], + "url": "localhost:9092", + }, + }, +} + + +def test_base_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = BaseSecurity(ssl_context=ssl_context) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == basic_schema + + +def test_plaintext_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = SASLPlaintext( + ssl_context=ssl_context, + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + plaintext_security_schema = deepcopy(basic_schema) + plaintext_security_schema["servers"]["development"]["security"] = [ + {"user-password": []}, + ] + plaintext_security_schema["components"]["securitySchemes"] = { + "user-password": {"type": "userPassword"}, + } + + assert schema == plaintext_security_schema + + +def test_scram256_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = SASLScram256( + ssl_context=ssl_context, + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + sasl256_security_schema = deepcopy(basic_schema) + sasl256_security_schema["servers"]["development"]["security"] = [{"scram256": []}] + sasl256_security_schema["components"]["securitySchemes"] = { + "scram256": {"type": "scramSha256"}, + } + + assert schema == sasl256_security_schema + + +def test_scram512_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = SASLScram512( + ssl_context=ssl_context, + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + sasl512_security_schema = deepcopy(basic_schema) + sasl512_security_schema["servers"]["development"]["security"] = [{"scram512": []}] + sasl512_security_schema["components"]["securitySchemes"] = { + "scram512": {"type": "scramSha512"}, + } + + assert schema == sasl512_security_schema + + +def test_oauthbearer_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = SASLOAuthBearer( + ssl_context=ssl_context, + ) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + sasl_oauthbearer_security_schema = deepcopy(basic_schema) + sasl_oauthbearer_security_schema["servers"]["development"]["security"] = [ + {"oauthbearer": []}, + ] + sasl_oauthbearer_security_schema["components"]["securitySchemes"] = { + "oauthbearer": {"type": "oauth2", "$ref": ""} + } + + assert schema == sasl_oauthbearer_security_schema + + +def test_gssapi_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = SASLGSSAPI( + ssl_context=ssl_context, + ) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + gssapi_security_schema = deepcopy(basic_schema) + gssapi_security_schema["servers"]["development"]["security"] = [{"gssapi": []}] + gssapi_security_schema["components"]["securitySchemes"] = { + "gssapi": {"type": "gssapi"}, + } + + assert schema == gssapi_security_schema diff --git a/tests/a_docs/confluent/additional_config/__init__.py b/tests/asyncapi/kafka/v3_0_0/__init__.py similarity index 100% rename from tests/a_docs/confluent/additional_config/__init__.py rename to tests/asyncapi/kafka/v3_0_0/__init__.py diff --git a/tests/asyncapi/kafka/v3_0_0/test_arguments.py b/tests/asyncapi/kafka/v3_0_0/test_arguments.py new file mode 100644 index 0000000000..6fa7a44400 --- /dev/null +++ b/tests/asyncapi/kafka/v3_0_0/test_arguments.py @@ -0,0 +1,20 @@ +from faststream.kafka import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.arguments import ArgumentsTestcase + + +class TestArguments(ArgumentsTestcase): + broker_factory = KafkaBroker + + def test_subscriber_bindings(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "kafka": {"bindingVersion": "0.4.0", "topic": "test"}, + } diff --git a/tests/asyncapi/kafka/v3_0_0/test_connection.py b/tests/asyncapi/kafka/v3_0_0/test_connection.py new file mode 100644 index 0000000000..e1fb6cfaab --- /dev/null +++ b/tests/asyncapi/kafka/v3_0_0/test_connection.py @@ -0,0 +1,98 @@ +from faststream.kafka import KafkaBroker +from faststream.specification import Tag +from faststream.specification.asyncapi import AsyncAPI + + +def test_base() -> None: + schema = AsyncAPI( + KafkaBroker( + "kafka:9092", + protocol="plaintext", + protocol_version="0.9.0", + description="Test description", + tags=(Tag(name="some-tag", description="experimental"),), + ), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "description": "Test description", + "protocol": "plaintext", + "protocolVersion": "0.9.0", + "tags": [{"description": "experimental", "name": "some-tag"}], + "host": "kafka:9092", + "pathname": "", + }, + }, + } + + +def test_multi() -> None: + schema = AsyncAPI( + KafkaBroker(["kafka:9092", "kafka:9093"]), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "kafka", + "protocolVersion": "auto", + "host": "kafka:9092", + "pathname": "", + }, + "Server2": { + "protocol": "kafka", + "protocolVersion": "auto", + "host": "kafka:9093", + "pathname": "", + }, + }, + } + + +def test_custom() -> None: + schema = AsyncAPI( + KafkaBroker( + ["kafka:9092", "kafka:9093"], + specification_url=["kafka:9094", "kafka:9095"], + ), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "kafka", + "protocolVersion": "auto", + "host": "kafka:9094", + "pathname": "", + }, + "Server2": { + "protocol": "kafka", + "protocolVersion": "auto", + "host": "kafka:9095", + "pathname": "", + }, + }, + } diff --git a/tests/asyncapi/kafka/v3_0_0/test_fastapi.py b/tests/asyncapi/kafka/v3_0_0/test_fastapi.py new file mode 100644 index 0000000000..32ce017bed --- /dev/null +++ b/tests/asyncapi/kafka/v3_0_0/test_fastapi.py @@ -0,0 +1,42 @@ +from faststream.kafka.fastapi import KafkaRouter +from faststream.kafka.testing import TestKafkaBroker +from faststream.security import SASLPlaintext +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.arguments import FastAPICompatible +from tests.asyncapi.base.v3_0_0.fastapi import FastAPITestCase +from tests.asyncapi.base.v3_0_0.publisher import PublisherTestcase + + +class TestRouterArguments(FastAPITestCase, FastAPICompatible): + broker_factory = staticmethod(lambda: KafkaRouter().broker) + router_factory = KafkaRouter + broker_wrapper = staticmethod(TestKafkaBroker) + + def build_app(self, router): + return router + + +class TestRouterPublisher(PublisherTestcase): + broker_factory = staticmethod(lambda: KafkaRouter().broker) + + def build_app(self, router): + return router + + +def test_fastapi_security_schema() -> None: + security = SASLPlaintext(username="user", password="pass", use_ssl=False) + + router = KafkaRouter("localhost:9092", security=security) + + schema = AsyncAPI(router.broker, schema_version="3.0.0").to_jsonable() + + assert schema["servers"]["development"] == { + "protocol": "kafka", + "protocolVersion": "auto", + "security": [{"user-password": []}], + "host": "localhost:9092", + "pathname": "", + } + assert schema["components"]["securitySchemes"] == { + "user-password": {"type": "userPassword"}, + } diff --git a/tests/asyncapi/kafka/v3_0_0/test_naming.py b/tests/asyncapi/kafka/v3_0_0/test_naming.py new file mode 100644 index 0000000000..e63d62cad9 --- /dev/null +++ b/tests/asyncapi/kafka/v3_0_0/test_naming.py @@ -0,0 +1,70 @@ +from faststream.kafka import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.naming import NamingTestCase + + +class TestNaming(NamingTestCase): + broker_class = KafkaBroker + + def test_base(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "defaultContentType": "application/json", + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "servers": { + "development": { + "host": "localhost", + "pathname": "", + "protocol": "kafka", + "protocolVersion": "auto", + }, + }, + "channels": { + "test:Handle": { + "address": "test:Handle", + "servers": [ + { + "$ref": "#/servers/development", + }, + ], + "bindings": {"kafka": {"topic": "test", "bindingVersion": "0.4.0"}}, + "messages": { + "SubscribeMessage": { + "$ref": "#/components/messages/test:Handle:SubscribeMessage", + }, + }, + }, + }, + "operations": { + "test:HandleSubscribe": { + "action": "receive", + "channel": { + "$ref": "#/channels/test:Handle", + }, + "messages": [ + { + "$ref": "#/channels/test:Handle/messages/SubscribeMessage", + }, + ], + }, + }, + "components": { + "messages": { + "test:Handle:SubscribeMessage": { + "title": "test:Handle:SubscribeMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/EmptyPayload"}, + }, + }, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + }, + } diff --git a/tests/asyncapi/kafka/v3_0_0/test_publisher.py b/tests/asyncapi/kafka/v3_0_0/test_publisher.py new file mode 100644 index 0000000000..1f6e6b8a08 --- /dev/null +++ b/tests/asyncapi/kafka/v3_0_0/test_publisher.py @@ -0,0 +1,20 @@ +from faststream.kafka import KafkaBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.publisher import PublisherTestcase + + +class TestArguments(PublisherTestcase): + broker_factory = KafkaBroker + + def test_publisher_bindings(self) -> None: + broker = self.broker_factory() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "kafka": {"bindingVersion": "0.4.0", "topic": "test"}, + } diff --git a/tests/asyncapi/kafka/v3_0_0/test_router.py b/tests/asyncapi/kafka/v3_0_0/test_router.py new file mode 100644 index 0000000000..ac1fed9404 --- /dev/null +++ b/tests/asyncapi/kafka/v3_0_0/test_router.py @@ -0,0 +1,97 @@ +from faststream.kafka import KafkaBroker, KafkaPublisher, KafkaRoute, KafkaRouter +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase +from tests.asyncapi.base.v3_0_0.router import RouterTestcase + + +class TestRouter(RouterTestcase): + broker_class = KafkaBroker + router_class = KafkaRouter + route_class = KafkaRoute + publisher_class = KafkaPublisher + + def test_prefix(self) -> None: + broker = self.broker_class() + + router = self.router_class(prefix="test_") + + @router.subscriber("test") + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert schema == { + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "asyncapi": "3.0.0", + "defaultContentType": "application/json", + "servers": { + "development": { + "host": "localhost", + "pathname": "", + "protocol": "kafka", + "protocolVersion": "auto", + }, + }, + "channels": { + "test_test:Handle": { + "address": "test_test:Handle", + "servers": [{"$ref": "#/servers/development"}], + "messages": { + "SubscribeMessage": { + "$ref": "#/components/messages/test_test:Handle:SubscribeMessage", + }, + }, + "bindings": { + "kafka": {"topic": "test_test", "bindingVersion": "0.4.0"}, + }, + }, + }, + "operations": { + "test_test:HandleSubscribe": { + "action": "receive", + "messages": [ + { + "$ref": "#/channels/test_test:Handle/messages/SubscribeMessage", + }, + ], + "channel": {"$ref": "#/channels/test_test:Handle"}, + }, + }, + "components": { + "messages": { + "test_test:Handle:SubscribeMessage": { + "title": "test_test:Handle:SubscribeMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": { + "$ref": "#/components/schemas/Handle:Message:Payload", + }, + }, + }, + "schemas": { + "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + }, + }, + } + + +class TestRouterArguments(ArgumentsTestcase): + broker_class = KafkaRouter + + def build_app(self, router): + broker = KafkaBroker() + broker.include_router(router) + return broker + + +class TestRouterPublisher(PublisherTestcase): + broker_class = KafkaRouter + + def build_app(self, router): + broker = KafkaBroker() + broker.include_router(router) + return broker diff --git a/tests/asyncapi/kafka/v3_0_0/test_security.py b/tests/asyncapi/kafka/v3_0_0/test_security.py new file mode 100644 index 0000000000..ddb06cce77 --- /dev/null +++ b/tests/asyncapi/kafka/v3_0_0/test_security.py @@ -0,0 +1,235 @@ +import ssl +from copy import deepcopy + +from faststream.kafka import KafkaBroker +from faststream.security import ( + SASLGSSAPI, + BaseSecurity, + SASLOAuthBearer, + SASLPlaintext, + SASLScram256, + SASLScram512, +) +from faststream.specification.asyncapi import AsyncAPI + +basic_schema = { + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "asyncapi": "3.0.0", + "defaultContentType": "application/json", + "servers": { + "development": { + "host": "localhost:9092", + "pathname": "", + "protocol": "kafka-secure", + "protocolVersion": "auto", + "security": [], + }, + }, + "channels": { + "test_1:TestTopic": { + "address": "test_1:TestTopic", + "servers": [{"$ref": "#/servers/development"}], + "messages": { + "SubscribeMessage": { + "$ref": "#/components/messages/test_1:TestTopic:SubscribeMessage", + }, + }, + "bindings": {"kafka": {"topic": "test_1", "bindingVersion": "0.4.0"}}, + }, + "test_2:Publisher": { + "address": "test_2:Publisher", + "servers": [{"$ref": "#/servers/development"}], + "messages": { + "Message": {"$ref": "#/components/messages/test_2:Publisher:Message"}, + }, + "bindings": {"kafka": {"topic": "test_2", "bindingVersion": "0.4.0"}}, + }, + }, + "operations": { + "test_1:TestTopicSubscribe": { + "action": "receive", + "messages": [ + {"$ref": "#/channels/test_1:TestTopic/messages/SubscribeMessage"}, + ], + "channel": {"$ref": "#/channels/test_1:TestTopic"}, + }, + "test_2:Publisher": { + "action": "send", + "messages": [{"$ref": "#/channels/test_2:Publisher/messages/Message"}], + "channel": {"$ref": "#/channels/test_2:Publisher"}, + }, + }, + "components": { + "messages": { + "test_1:TestTopic:SubscribeMessage": { + "title": "test_1:TestTopic:SubscribeMessage", + "correlationId": {"location": "$message.header#/correlation_id"}, + "payload": {"$ref": "#/components/schemas/TestTopic:Message:Payload"}, + }, + "test_2:Publisher:Message": { + "title": "test_2:Publisher:Message", + "correlationId": {"location": "$message.header#/correlation_id"}, + "payload": { + "$ref": "#/components/schemas/test_2:Publisher:Message:Payload", + }, + }, + }, + "schemas": { + "TestTopic:Message:Payload": { + "title": "TestTopic:Message:Payload", + "type": "string", + }, + "test_2:Publisher:Message:Payload": { + "title": "test_2:Publisher:Message:Payload", + "type": "string", + }, + }, + "securitySchemes": {}, + }, +} + + +def test_base_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = BaseSecurity(ssl_context=ssl_context) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert schema == basic_schema + + +def test_plaintext_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = SASLPlaintext( + ssl_context=ssl_context, + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + plaintext_security_schema = deepcopy(basic_schema) + plaintext_security_schema["servers"]["development"]["security"] = [ + {"user-password": []}, + ] + plaintext_security_schema["components"]["securitySchemes"] = { + "user-password": {"type": "userPassword"}, + } + + assert schema == plaintext_security_schema + + +def test_scram256_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = SASLScram256( + ssl_context=ssl_context, + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + sasl256_security_schema = deepcopy(basic_schema) + sasl256_security_schema["servers"]["development"]["security"] = [{"scram256": []}] + sasl256_security_schema["components"]["securitySchemes"] = { + "scram256": {"type": "scramSha256"}, + } + + assert schema == sasl256_security_schema + + +def test_scram512_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = SASLScram512( + ssl_context=ssl_context, + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + sasl512_security_schema = deepcopy(basic_schema) + sasl512_security_schema["servers"]["development"]["security"] = [{"scram512": []}] + sasl512_security_schema["components"]["securitySchemes"] = { + "scram512": {"type": "scramSha512"}, + } + + assert schema == sasl512_security_schema + + +def test_oauthbearer_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = SASLOAuthBearer( + ssl_context=ssl_context, + ) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + sasl_oauthbearer_security_schema = deepcopy(basic_schema) + sasl_oauthbearer_security_schema["servers"]["development"]["security"] = [ + {"oauthbearer": []}, + ] + sasl_oauthbearer_security_schema["components"]["securitySchemes"] = { + "oauthbearer": {"type": "oauth2", "$ref": ""} + } + + assert schema == sasl_oauthbearer_security_schema + + +def test_gssapi_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = SASLGSSAPI( + ssl_context=ssl_context, + ) + + broker = KafkaBroker("localhost:9092", security=security) + + @broker.publisher("test_2") + @broker.subscriber("test_1") + async def test_topic(msg: str) -> str: + pass + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + gssapi_security_schema = deepcopy(basic_schema) + gssapi_security_schema["servers"]["development"]["security"] = [{"gssapi": []}] + gssapi_security_schema["components"]["securitySchemes"] = { + "gssapi": {"type": "gssapi"}, + } + + assert schema == gssapi_security_schema diff --git a/tests/asyncapi/nats/test_arguments.py b/tests/asyncapi/nats/test_arguments.py deleted file mode 100644 index 4749b85b5a..0000000000 --- a/tests/asyncapi/nats/test_arguments.py +++ /dev/null @@ -1,20 +0,0 @@ -from faststream.asyncapi.generate import get_app_schema -from faststream.nats import NatsBroker -from tests.asyncapi.base.arguments import ArgumentsTestcase - - -class TestArguments(ArgumentsTestcase): - broker_class = NatsBroker - - def test_subscriber_bindings(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "nats": {"bindingVersion": "custom", "subject": "test"} - } diff --git a/tests/asyncapi/nats/test_connection.py b/tests/asyncapi/nats/test_connection.py deleted file mode 100644 index 0f1f5c057e..0000000000 --- a/tests/asyncapi/nats/test_connection.py +++ /dev/null @@ -1,91 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.asyncapi.schema import Tag -from faststream.nats import NatsBroker - - -def test_base(): - schema = get_app_schema( - FastStream( - NatsBroker( - "nats:9092", - protocol="plaintext", - protocol_version="0.9.0", - description="Test description", - tags=(Tag(name="some-tag", description="experimental"),), - ) - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "description": "Test description", - "protocol": "plaintext", - "protocolVersion": "0.9.0", - "tags": [{"description": "experimental", "name": "some-tag"}], - "url": "nats:9092", - } - }, - }, schema - - -def test_multi(): - schema = get_app_schema( - FastStream(NatsBroker(["nats:9092", "nats:9093"])) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "Server1": { - "protocol": "nats", - "protocolVersion": "custom", - "url": "nats:9092", - }, - "Server2": { - "protocol": "nats", - "protocolVersion": "custom", - "url": "nats:9093", - }, - }, - } - - -def test_custom(): - schema = get_app_schema( - FastStream( - NatsBroker( - ["nats:9092", "nats:9093"], asyncapi_url=["nats:9094", "nats:9095"] - ) - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "Server1": { - "protocol": "nats", - "protocolVersion": "custom", - "url": "nats:9094", - }, - "Server2": { - "protocol": "nats", - "protocolVersion": "custom", - "url": "nats:9095", - }, - }, - } diff --git a/tests/asyncapi/nats/test_fastapi.py b/tests/asyncapi/nats/test_fastapi.py deleted file mode 100644 index 3b4a777523..0000000000 --- a/tests/asyncapi/nats/test_fastapi.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Type - -from faststream.nats import TestNatsBroker -from faststream.nats.fastapi import NatsRouter -from tests.asyncapi.base.arguments import FastAPICompatible -from tests.asyncapi.base.fastapi import FastAPITestCase -from tests.asyncapi.base.publisher import PublisherTestcase - - -class TestRouterArguments(FastAPITestCase, FastAPICompatible): - broker_class: Type[NatsRouter] = NatsRouter - broker_wrapper = staticmethod(TestNatsBroker) - - def build_app(self, router): - return router - - -class TestRouterPublisher(PublisherTestcase): - broker_class = NatsRouter - - def build_app(self, router): - return router diff --git a/tests/asyncapi/nats/test_kv_schema.py b/tests/asyncapi/nats/test_kv_schema.py deleted file mode 100644 index 4b0edc1847..0000000000 --- a/tests/asyncapi/nats/test_kv_schema.py +++ /dev/null @@ -1,14 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.nats import NatsBroker - - -def test_kv_schema(): - broker = NatsBroker() - - @broker.subscriber("test", kv_watch="test") - async def handle(): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema["channels"] == {} diff --git a/tests/asyncapi/nats/test_naming.py b/tests/asyncapi/nats/test_naming.py deleted file mode 100644 index 833289e8db..0000000000 --- a/tests/asyncapi/nats/test_naming.py +++ /dev/null @@ -1,52 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.nats import NatsBroker -from tests.asyncapi.base.naming import NamingTestCase - - -class TestNaming(NamingTestCase): - broker_class = NatsBroker - - def test_base(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "defaultContentType": "application/json", - "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, - "servers": { - "development": { - "url": "nats://localhost:4222", - "protocol": "nats", - "protocolVersion": "custom", - } - }, - "channels": { - "test:Handle": { - "servers": ["development"], - "bindings": { - "nats": {"subject": "test", "bindingVersion": "custom"} - }, - "subscribe": { - "message": {"$ref": "#/components/messages/test:Handle:Message"} - }, - } - }, - "components": { - "messages": { - "test:Handle:Message": { - "title": "test:Handle:Message", - "correlationId": { - "location": "$message.header#/correlation_id" - }, - "payload": {"$ref": "#/components/schemas/EmptyPayload"}, - } - }, - "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, - }, - } diff --git a/tests/asyncapi/nats/test_obj_schema.py b/tests/asyncapi/nats/test_obj_schema.py deleted file mode 100644 index f7546cbc22..0000000000 --- a/tests/asyncapi/nats/test_obj_schema.py +++ /dev/null @@ -1,14 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.nats import NatsBroker - - -def test_obj_schema(): - broker = NatsBroker() - - @broker.subscriber("test", obj_watch=True) - async def handle(): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema["channels"] == {} diff --git a/tests/asyncapi/nats/test_publisher.py b/tests/asyncapi/nats/test_publisher.py deleted file mode 100644 index 5263a0dd99..0000000000 --- a/tests/asyncapi/nats/test_publisher.py +++ /dev/null @@ -1,20 +0,0 @@ -from faststream.asyncapi.generate import get_app_schema -from faststream.nats import NatsBroker -from tests.asyncapi.base.publisher import PublisherTestcase - - -class TestArguments(PublisherTestcase): - broker_class = NatsBroker - - def test_publisher_bindings(self): - broker = self.broker_class() - - @broker.publisher("test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "nats": {"bindingVersion": "custom", "subject": "test"} - }, schema["channels"][key]["bindings"] diff --git a/tests/asyncapi/nats/test_router.py b/tests/asyncapi/nats/test_router.py deleted file mode 100644 index 19087d14de..0000000000 --- a/tests/asyncapi/nats/test_router.py +++ /dev/null @@ -1,85 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.nats import NatsBroker, NatsPublisher, NatsRoute, NatsRouter -from tests.asyncapi.base.arguments import ArgumentsTestcase -from tests.asyncapi.base.publisher import PublisherTestcase -from tests.asyncapi.base.router import RouterTestcase - - -class TestRouter(RouterTestcase): - broker_class = NatsBroker - router_class = NatsRouter - route_class = NatsRoute - publisher_class = NatsPublisher - - def test_prefix(self): - broker = self.broker_class() - - router = self.router_class(prefix="test_") - - @router.subscriber("test") - async def handle(msg): ... - - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "defaultContentType": "application/json", - "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, - "servers": { - "development": { - "url": "nats://localhost:4222", - "protocol": "nats", - "protocolVersion": "custom", - } - }, - "channels": { - "test_test:Handle": { - "servers": ["development"], - "bindings": { - "nats": {"subject": "test_test", "bindingVersion": "custom"} - }, - "subscribe": { - "message": { - "$ref": "#/components/messages/test_test:Handle:Message" - } - }, - } - }, - "components": { - "messages": { - "test_test:Handle:Message": { - "title": "test_test:Handle:Message", - "correlationId": { - "location": "$message.header#/correlation_id" - }, - "payload": { - "$ref": "#/components/schemas/Handle:Message:Payload" - }, - } - }, - "schemas": { - "Handle:Message:Payload": {"title": "Handle:Message:Payload"} - }, - }, - } - - -class TestRouterArguments(ArgumentsTestcase): - broker_class = NatsRouter - - def build_app(self, router): - broker = NatsBroker() - broker.include_router(router) - return FastStream(broker) - - -class TestRouterPublisher(PublisherTestcase): - broker_class = NatsRouter - - def build_app(self, router): - broker = NatsBroker() - broker.include_router(router) - return FastStream(broker) diff --git a/tests/a_docs/getting_started/cli/nats/__init__.py b/tests/asyncapi/nats/v2_6_0/__init__.py similarity index 100% rename from tests/a_docs/getting_started/cli/nats/__init__.py rename to tests/asyncapi/nats/v2_6_0/__init__.py diff --git a/tests/asyncapi/nats/v2_6_0/test_arguments.py b/tests/asyncapi/nats/v2_6_0/test_arguments.py new file mode 100644 index 0000000000..5ad34a0001 --- /dev/null +++ b/tests/asyncapi/nats/v2_6_0/test_arguments.py @@ -0,0 +1,20 @@ +from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase + + +class TestArguments(ArgumentsTestcase): + broker_class = NatsBroker + + def test_subscriber_bindings(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + 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 new file mode 100644 index 0000000000..486bbb8033 --- /dev/null +++ b/tests/asyncapi/nats/v2_6_0/test_connection.py @@ -0,0 +1,90 @@ +from faststream.nats import NatsBroker +from faststream.specification import Tag +from faststream.specification.asyncapi import AsyncAPI + + +def test_base() -> None: + schema = AsyncAPI( + NatsBroker( + "nats:9092", + protocol="plaintext", + protocol_version="0.9.0", + description="Test description", + tags=(Tag(name="some-tag", description="experimental"),), + ), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "description": "Test description", + "protocol": "plaintext", + "protocolVersion": "0.9.0", + "tags": [{"description": "experimental", "name": "some-tag"}], + "url": "nats:9092", + }, + }, + }, schema + + +def test_multi() -> None: + schema = AsyncAPI( + NatsBroker(["nats:9092", "nats:9093"]), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "nats", + "protocolVersion": "custom", + "url": "nats:9092", + }, + "Server2": { + "protocol": "nats", + "protocolVersion": "custom", + "url": "nats:9093", + }, + }, + } + + +def test_custom() -> None: + schema = AsyncAPI( + NatsBroker( + ["nats:9092", "nats:9093"], + specification_url=["nats:9094", "nats:9095"], + ), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "nats", + "protocolVersion": "custom", + "url": "nats:9094", + }, + "Server2": { + "protocol": "nats", + "protocolVersion": "custom", + "url": "nats:9095", + }, + }, + } diff --git a/tests/asyncapi/nats/v2_6_0/test_fastapi.py b/tests/asyncapi/nats/v2_6_0/test_fastapi.py new file mode 100644 index 0000000000..6f7c8b3eb1 --- /dev/null +++ b/tests/asyncapi/nats/v2_6_0/test_fastapi.py @@ -0,0 +1,21 @@ +from faststream.nats import TestNatsBroker +from faststream.nats.fastapi import NatsRouter +from tests.asyncapi.base.v2_6_0.arguments import FastAPICompatible +from tests.asyncapi.base.v2_6_0.fastapi import FastAPITestCase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase + + +class TestRouterArguments(FastAPITestCase, FastAPICompatible): + broker_class = staticmethod(lambda: NatsRouter().broker) + router_class = NatsRouter + broker_wrapper = staticmethod(TestNatsBroker) + + def build_app(self, router): + return router + + +class TestRouterPublisher(PublisherTestcase): + broker_class = staticmethod(lambda: NatsRouter().broker) + + def build_app(self, router): + return router diff --git a/tests/asyncapi/nats/v2_6_0/test_kv_schema.py b/tests/asyncapi/nats/v2_6_0/test_kv_schema.py new file mode 100644 index 0000000000..f069f9b476 --- /dev/null +++ b/tests/asyncapi/nats/v2_6_0/test_kv_schema.py @@ -0,0 +1,13 @@ +from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI + + +def test_kv_schema() -> None: + broker = NatsBroker() + + @broker.subscriber("test", kv_watch="test") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema["channels"] == {} diff --git a/tests/asyncapi/nats/v2_6_0/test_naming.py b/tests/asyncapi/nats/v2_6_0/test_naming.py new file mode 100644 index 0000000000..9c0738f9de --- /dev/null +++ b/tests/asyncapi/nats/v2_6_0/test_naming.py @@ -0,0 +1,53 @@ +from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.naming import NamingTestCase + + +class TestNaming(NamingTestCase): + broker_class = NatsBroker + + def test_base(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "defaultContentType": "application/json", + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "servers": { + "development": { + "url": "nats://localhost:4222", + "protocol": "nats", + "protocolVersion": "custom", + }, + }, + "channels": { + "test:Handle": { + "servers": ["development"], + "bindings": { + "nats": {"subject": "test", "bindingVersion": "custom"}, + }, + "publish": { + "message": { + "$ref": "#/components/messages/test:Handle:Message" + }, + }, + }, + }, + "components": { + "messages": { + "test:Handle:Message": { + "title": "test:Handle:Message", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/EmptyPayload"}, + }, + }, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + }, + } diff --git a/tests/asyncapi/nats/v2_6_0/test_obj_schema.py b/tests/asyncapi/nats/v2_6_0/test_obj_schema.py new file mode 100644 index 0000000000..51d5507bb4 --- /dev/null +++ b/tests/asyncapi/nats/v2_6_0/test_obj_schema.py @@ -0,0 +1,13 @@ +from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI + + +def test_obj_schema() -> None: + broker = NatsBroker() + + @broker.subscriber("test", obj_watch=True) + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema["channels"] == {} diff --git a/tests/asyncapi/nats/v2_6_0/test_publisher.py b/tests/asyncapi/nats/v2_6_0/test_publisher.py new file mode 100644 index 0000000000..cdf7291ab7 --- /dev/null +++ b/tests/asyncapi/nats/v2_6_0/test_publisher.py @@ -0,0 +1,20 @@ +from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase + + +class TestArguments(PublisherTestcase): + broker_class = NatsBroker + + def test_publisher_bindings(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "nats": {"bindingVersion": "custom", "subject": "test"}, + }, schema["channels"][key]["bindings"] diff --git a/tests/asyncapi/nats/v2_6_0/test_router.py b/tests/asyncapi/nats/v2_6_0/test_router.py new file mode 100644 index 0000000000..7986cba82e --- /dev/null +++ b/tests/asyncapi/nats/v2_6_0/test_router.py @@ -0,0 +1,84 @@ +from faststream.nats import NatsBroker, NatsPublisher, NatsRoute, NatsRouter +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase +from tests.asyncapi.base.v2_6_0.router import RouterTestcase + + +class TestRouter(RouterTestcase): + broker_class = NatsBroker + router_class = NatsRouter + route_class = NatsRoute + publisher_class = NatsPublisher + + def test_prefix(self) -> None: + broker = self.broker_class() + + router = self.router_class(prefix="test_") + + @router.subscriber("test") + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "defaultContentType": "application/json", + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "servers": { + "development": { + "url": "nats://localhost:4222", + "protocol": "nats", + "protocolVersion": "custom", + }, + }, + "channels": { + "test_test:Handle": { + "servers": ["development"], + "bindings": { + "nats": {"subject": "test_test", "bindingVersion": "custom"}, + }, + "publish": { + "message": { + "$ref": "#/components/messages/test_test:Handle:Message", + }, + }, + }, + }, + "components": { + "messages": { + "test_test:Handle:Message": { + "title": "test_test:Handle:Message", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": { + "$ref": "#/components/schemas/Handle:Message:Payload", + }, + }, + }, + "schemas": { + "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + }, + }, + } + + +class TestRouterArguments(ArgumentsTestcase): + broker_class = NatsRouter + + def build_app(self, router): + broker = NatsBroker() + broker.include_router(router) + return broker + + +class TestRouterPublisher(PublisherTestcase): + broker_class = NatsRouter + + def build_app(self, router): + broker = NatsBroker() + broker.include_router(router) + return broker diff --git a/tests/a_docs/nats/__init__.py b/tests/asyncapi/nats/v3_0_0/__init__.py similarity index 100% rename from tests/a_docs/nats/__init__.py rename to tests/asyncapi/nats/v3_0_0/__init__.py diff --git a/tests/asyncapi/nats/v3_0_0/test_arguments.py b/tests/asyncapi/nats/v3_0_0/test_arguments.py new file mode 100644 index 0000000000..837ad7bdcc --- /dev/null +++ b/tests/asyncapi/nats/v3_0_0/test_arguments.py @@ -0,0 +1,20 @@ +from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.arguments import ArgumentsTestcase + + +class TestArguments(ArgumentsTestcase): + broker_factory = NatsBroker + + def test_subscriber_bindings(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "nats": {"bindingVersion": "custom", "subject": "test"}, + } diff --git a/tests/asyncapi/nats/v3_0_0/test_connection.py b/tests/asyncapi/nats/v3_0_0/test_connection.py new file mode 100644 index 0000000000..f88fc0fb83 --- /dev/null +++ b/tests/asyncapi/nats/v3_0_0/test_connection.py @@ -0,0 +1,98 @@ +from faststream.nats import NatsBroker +from faststream.specification import Tag +from faststream.specification.asyncapi import AsyncAPI + + +def test_base() -> None: + schema = AsyncAPI( + NatsBroker( + "nats:9092", + protocol="plaintext", + protocol_version="0.9.0", + description="Test description", + tags=(Tag(name="some-tag", description="experimental"),), + ), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "description": "Test description", + "protocol": "plaintext", + "protocolVersion": "0.9.0", + "tags": [{"description": "experimental", "name": "some-tag"}], + "host": "nats:9092", + "pathname": "", + }, + }, + }, schema + + +def test_multi() -> None: + schema = AsyncAPI( + NatsBroker(["nats:9092", "nats:9093"]), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "nats", + "protocolVersion": "custom", + "host": "nats:9092", + "pathname": "", + }, + "Server2": { + "protocol": "nats", + "protocolVersion": "custom", + "host": "nats:9093", + "pathname": "", + }, + }, + } + + +def test_custom() -> None: + schema = AsyncAPI( + NatsBroker( + ["nats:9092", "nats:9093"], + specification_url=["nats:9094", "nats:9095"], + ), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "Server1": { + "protocol": "nats", + "protocolVersion": "custom", + "host": "nats:9094", + "pathname": "", + }, + "Server2": { + "protocol": "nats", + "protocolVersion": "custom", + "host": "nats:9095", + "pathname": "", + }, + }, + } diff --git a/tests/asyncapi/nats/v3_0_0/test_fastapi.py b/tests/asyncapi/nats/v3_0_0/test_fastapi.py new file mode 100644 index 0000000000..2bd8cc25c4 --- /dev/null +++ b/tests/asyncapi/nats/v3_0_0/test_fastapi.py @@ -0,0 +1,21 @@ +from faststream.nats import TestNatsBroker +from faststream.nats.fastapi import NatsRouter +from tests.asyncapi.base.v3_0_0.arguments import FastAPICompatible +from tests.asyncapi.base.v3_0_0.fastapi import FastAPITestCase +from tests.asyncapi.base.v3_0_0.publisher import PublisherTestcase + + +class TestRouterArguments(FastAPITestCase, FastAPICompatible): + broker_factory = staticmethod(lambda: NatsRouter().broker) + router_factory = NatsRouter + broker_wrapper = staticmethod(TestNatsBroker) + + def build_app(self, router): + return router + + +class TestRouterPublisher(PublisherTestcase): + broker_factory = staticmethod(lambda: NatsRouter().broker) + + def build_app(self, router): + return router diff --git a/tests/asyncapi/nats/v3_0_0/test_kv_schema.py b/tests/asyncapi/nats/v3_0_0/test_kv_schema.py new file mode 100644 index 0000000000..bcd0c0e158 --- /dev/null +++ b/tests/asyncapi/nats/v3_0_0/test_kv_schema.py @@ -0,0 +1,13 @@ +from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI + + +def test_kv_schema() -> None: + broker = NatsBroker() + + @broker.subscriber("test", kv_watch="test") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert schema["channels"] == {} diff --git a/tests/asyncapi/nats/v3_0_0/test_naming.py b/tests/asyncapi/nats/v3_0_0/test_naming.py new file mode 100644 index 0000000000..b28fea104f --- /dev/null +++ b/tests/asyncapi/nats/v3_0_0/test_naming.py @@ -0,0 +1,72 @@ +from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.naming import NamingTestCase + + +class TestNaming(NamingTestCase): + broker_class = NatsBroker + + def test_base(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "defaultContentType": "application/json", + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "servers": { + "development": { + "host": "localhost:4222", + "pathname": "", + "protocol": "nats", + "protocolVersion": "custom", + }, + }, + "channels": { + "test:Handle": { + "address": "test:Handle", + "servers": [ + { + "$ref": "#/servers/development", + }, + ], + "bindings": { + "nats": {"subject": "test", "bindingVersion": "custom"}, + }, + "messages": { + "SubscribeMessage": { + "$ref": "#/components/messages/test:Handle:SubscribeMessage", + }, + }, + }, + }, + "operations": { + "test:HandleSubscribe": { + "action": "receive", + "channel": { + "$ref": "#/channels/test:Handle", + }, + "messages": [ + { + "$ref": "#/channels/test:Handle/messages/SubscribeMessage", + }, + ], + }, + }, + "components": { + "messages": { + "test:Handle:SubscribeMessage": { + "title": "test:Handle:SubscribeMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/EmptyPayload"}, + }, + }, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + }, + } diff --git a/tests/asyncapi/nats/v3_0_0/test_obj_schema.py b/tests/asyncapi/nats/v3_0_0/test_obj_schema.py new file mode 100644 index 0000000000..d3b434ddee --- /dev/null +++ b/tests/asyncapi/nats/v3_0_0/test_obj_schema.py @@ -0,0 +1,13 @@ +from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI + + +def test_obj_schema() -> None: + broker = NatsBroker() + + @broker.subscriber("test", obj_watch=True) + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert schema["channels"] == {} diff --git a/tests/asyncapi/nats/v3_0_0/test_publisher.py b/tests/asyncapi/nats/v3_0_0/test_publisher.py new file mode 100644 index 0000000000..9a83756c09 --- /dev/null +++ b/tests/asyncapi/nats/v3_0_0/test_publisher.py @@ -0,0 +1,20 @@ +from faststream.nats import NatsBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.publisher import PublisherTestcase + + +class TestArguments(PublisherTestcase): + broker_factory = NatsBroker + + def test_publisher_bindings(self) -> None: + broker = self.broker_factory() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "nats": {"bindingVersion": "custom", "subject": "test"}, + }, schema["channels"][key]["bindings"] diff --git a/tests/asyncapi/nats/v3_0_0/test_router.py b/tests/asyncapi/nats/v3_0_0/test_router.py new file mode 100644 index 0000000000..a075322b60 --- /dev/null +++ b/tests/asyncapi/nats/v3_0_0/test_router.py @@ -0,0 +1,97 @@ +from faststream.nats import NatsBroker, NatsPublisher, NatsRoute, NatsRouter +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase +from tests.asyncapi.base.v3_0_0.router import RouterTestcase + + +class TestRouter(RouterTestcase): + broker_class = NatsBroker + router_class = NatsRouter + route_class = NatsRoute + publisher_class = NatsPublisher + + def test_prefix(self) -> None: + broker = self.broker_class() + + router = self.router_class(prefix="test_") + + @router.subscriber("test") + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert schema == { + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "asyncapi": "3.0.0", + "defaultContentType": "application/json", + "servers": { + "development": { + "host": "localhost:4222", + "pathname": "", + "protocol": "nats", + "protocolVersion": "custom", + }, + }, + "channels": { + "test_test:Handle": { + "address": "test_test:Handle", + "servers": [{"$ref": "#/servers/development"}], + "messages": { + "SubscribeMessage": { + "$ref": "#/components/messages/test_test:Handle:SubscribeMessage", + }, + }, + "bindings": { + "nats": {"subject": "test_test", "bindingVersion": "custom"}, + }, + }, + }, + "operations": { + "test_test:HandleSubscribe": { + "action": "receive", + "messages": [ + { + "$ref": "#/channels/test_test:Handle/messages/SubscribeMessage", + }, + ], + "channel": {"$ref": "#/channels/test_test:Handle"}, + }, + }, + "components": { + "messages": { + "test_test:Handle:SubscribeMessage": { + "title": "test_test:Handle:SubscribeMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": { + "$ref": "#/components/schemas/Handle:Message:Payload", + }, + }, + }, + "schemas": { + "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + }, + }, + } + + +class TestRouterArguments(ArgumentsTestcase): + broker_class = NatsRouter + + def build_app(self, router): + broker = NatsBroker() + broker.include_router(router) + return broker + + +class TestRouterPublisher(PublisherTestcase): + broker_class = NatsRouter + + def build_app(self, router): + broker = NatsBroker() + broker.include_router(router) + return broker diff --git a/tests/asyncapi/rabbit/test_arguments.py b/tests/asyncapi/rabbit/test_arguments.py deleted file mode 100644 index f192b43766..0000000000 --- a/tests/asyncapi/rabbit/test_arguments.py +++ /dev/null @@ -1,170 +0,0 @@ -from faststream.asyncapi.generate import get_app_schema -from faststream.rabbit import ExchangeType, RabbitBroker, RabbitExchange, RabbitQueue -from tests.asyncapi.base.arguments import ArgumentsTestcase - - -class TestArguments(ArgumentsTestcase): - broker_class = RabbitBroker - - def test_subscriber_bindings(self): - broker = self.broker_class() - - @broker.subscriber( - RabbitQueue("test", auto_delete=True), - RabbitExchange("test-ex", type=ExchangeType.TOPIC), - ) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": { - "autoDelete": False, - "durable": False, - "name": "test-ex", - "type": "topic", - "vhost": "/", - }, - "is": "routingKey", - "queue": { - "autoDelete": True, - "durable": False, - "exclusive": False, - "name": "test", - "vhost": "/", - }, - } - } - - def test_subscriber_fanout_bindings(self): - broker = self.broker_class() - - @broker.subscriber( - RabbitQueue("test", auto_delete=True), - RabbitExchange("test-ex", type=ExchangeType.FANOUT), - ) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": { - "autoDelete": False, - "durable": False, - "name": "test-ex", - "type": "fanout", - "vhost": "/", - }, - "is": "routingKey", - } - } - - def test_subscriber_headers_bindings(self): - broker = self.broker_class() - - @broker.subscriber( - RabbitQueue("test", auto_delete=True), - RabbitExchange("test-ex", type=ExchangeType.HEADERS), - ) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": { - "autoDelete": False, - "durable": False, - "name": "test-ex", - "type": "headers", - "vhost": "/", - }, - "is": "routingKey", - } - } - - def test_subscriber_xdelay_bindings(self): - broker = self.broker_class() - - @broker.subscriber( - RabbitQueue("test", auto_delete=True), - RabbitExchange("test-ex", type=ExchangeType.X_DELAYED_MESSAGE), - ) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": { - "autoDelete": False, - "durable": False, - "name": "test-ex", - "type": "x-delayed-message", - "vhost": "/", - }, - "is": "routingKey", - } - } - - def test_subscriber_consistent_hash_bindings(self): - broker = self.broker_class() - - @broker.subscriber( - RabbitQueue("test", auto_delete=True), - RabbitExchange("test-ex", type=ExchangeType.X_CONSISTENT_HASH), - ) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": { - "autoDelete": False, - "durable": False, - "name": "test-ex", - "type": "x-consistent-hash", - "vhost": "/", - }, - "is": "routingKey", - } - } - - def test_subscriber_modules_hash_bindings(self): - broker = self.broker_class() - - @broker.subscriber( - RabbitQueue("test", auto_delete=True), - RabbitExchange("test-ex", type=ExchangeType.X_MODULUS_HASH), - ) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": { - "autoDelete": False, - "durable": False, - "name": "test-ex", - "type": "x-modulus-hash", - "vhost": "/", - }, - "is": "routingKey", - } - } diff --git a/tests/asyncapi/rabbit/test_connection.py b/tests/asyncapi/rabbit/test_connection.py deleted file mode 100644 index 4362e8ac48..0000000000 --- a/tests/asyncapi/rabbit/test_connection.py +++ /dev/null @@ -1,120 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.asyncapi.schema import Tag -from faststream.rabbit import RabbitBroker - - -def test_base(): - schema = get_app_schema( - FastStream( - RabbitBroker( - "amqps://localhost", - port=5673, - protocol_version="0.9.0", - description="Test description", - tags=(Tag(name="some-tag", description="experimental"),), - ) - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "description": "Test description", - "protocol": "amqps", - "protocolVersion": "0.9.0", - "tags": [{"description": "experimental", "name": "some-tag"}], - "url": "amqps://guest:guest@localhost:5673/", # pragma: allowlist secret - } - }, - } - - -def test_kwargs(): - broker = RabbitBroker( - "amqp://guest:guest@localhost:5672/?heartbeat=300", # pragma: allowlist secret - host="127.0.0.1", - ) - - assert ( - broker.url - == "amqp://guest:guest@127.0.0.1:5672/?heartbeat=300" # pragma: allowlist secret - ) - - -def test_custom(): - broker = RabbitBroker( - "amqps://localhost", - asyncapi_url="amqp://guest:guest@127.0.0.1:5672/vh", # pragma: allowlist secret - ) - - broker.publisher("test") - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert ( - schema - == { - "asyncapi": "2.6.0", - "channels": { - "test:_:Publisher": { - "bindings": { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": {"type": "default", "vhost": "/vh"}, - "is": "routingKey", - "queue": { - "autoDelete": False, - "durable": False, - "exclusive": False, - "name": "test", - "vhost": "/vh", - }, - } - }, - "publish": { - "bindings": { - "amqp": { - "ack": True, - "bindingVersion": "0.2.0", - "cc": "test", - "deliveryMode": 1, - "mandatory": True, - } - }, - "message": { - "$ref": "#/components/messages/test:_:Publisher:Message" - }, - }, - "servers": ["development"], - } - }, - "components": { - "messages": { - "test:_:Publisher:Message": { - "correlationId": { - "location": "$message.header#/correlation_id" - }, - "payload": { - "$ref": "#/components/schemas/test:_:PublisherPayload" - }, - "title": "test:_:Publisher:Message", - } - }, - "schemas": {"test:_:PublisherPayload": {}}, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "amqp", - "protocolVersion": "0.9.1", - "url": "amqp://guest:guest@127.0.0.1:5672/vh", # pragma: allowlist secret - } - }, - } - ) diff --git a/tests/asyncapi/rabbit/test_fastapi.py b/tests/asyncapi/rabbit/test_fastapi.py deleted file mode 100644 index e205f9966e..0000000000 --- a/tests/asyncapi/rabbit/test_fastapi.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Type - -from faststream.asyncapi.generate import get_app_schema -from faststream.rabbit.fastapi import RabbitRouter -from faststream.rabbit.testing import TestRabbitBroker -from faststream.security import SASLPlaintext -from tests.asyncapi.base.arguments import FastAPICompatible -from tests.asyncapi.base.fastapi import FastAPITestCase -from tests.asyncapi.base.publisher import PublisherTestcase - - -class TestRouterArguments(FastAPITestCase, FastAPICompatible): - broker_class: Type[RabbitRouter] = RabbitRouter - broker_wrapper = staticmethod(TestRabbitBroker) - - def build_app(self, router): - return router - - -class TestRouterPublisher(PublisherTestcase): - broker_class = RabbitRouter - - def build_app(self, router): - return router - - -def test_fastapi_security_schema(): - security = SASLPlaintext(username="user", password="pass", use_ssl=False) - - broker = RabbitRouter(security=security) - - schema = get_app_schema(broker).to_jsonable() - - assert schema["servers"]["development"] == { - "protocol": "amqp", - "protocolVersion": "0.9.1", - "security": [{"user-password": []}], - "url": "amqp://user:pass@localhost:5672/", # pragma: allowlist secret - } - assert schema["components"]["securitySchemes"] == { - "user-password": {"type": "userPassword"} - } diff --git a/tests/asyncapi/rabbit/test_naming.py b/tests/asyncapi/rabbit/test_naming.py deleted file mode 100644 index b97965649c..0000000000 --- a/tests/asyncapi/rabbit/test_naming.py +++ /dev/null @@ -1,107 +0,0 @@ -from typing import Type - -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.rabbit import RabbitBroker -from tests.asyncapi.base.naming import NamingTestCase - - -class TestNaming(NamingTestCase): - broker_class: Type[RabbitBroker] = RabbitBroker - - def test_subscriber_with_exchange(self): - broker = self.broker_class() - - @broker.subscriber("test", "exchange") - async def handle(): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == ["test:exchange:Handle"] - - assert list(schema["components"]["messages"].keys()) == [ - "test:exchange:Handle:Message" - ] - - def test_publisher_with_exchange(self): - broker = self.broker_class() - - @broker.publisher("test", "exchange") - async def handle(): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert list(schema["channels"].keys()) == ["test:exchange:Publisher"] - - assert list(schema["components"]["messages"].keys()) == [ - "test:exchange:Publisher:Message" - ] - - def test_base(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert ( - schema - == { - "asyncapi": "2.6.0", - "defaultContentType": "application/json", - "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, - "servers": { - "development": { - "url": "amqp://guest:guest@localhost:5672/", # pragma: allowlist secret - "protocol": "amqp", - "protocolVersion": "0.9.1", - } - }, - "channels": { - "test:_:Handle": { - "servers": ["development"], - "bindings": { - "amqp": { - "is": "routingKey", - "bindingVersion": "0.2.0", - "queue": { - "name": "test", - "durable": False, - "exclusive": False, - "autoDelete": False, - "vhost": "/", - }, - "exchange": {"type": "default", "vhost": "/"}, - } - }, - "subscribe": { - "bindings": { - "amqp": { - "cc": "test", - "ack": True, - "bindingVersion": "0.2.0", - } - }, - "message": { - "$ref": "#/components/messages/test:_:Handle:Message" - }, - }, - } - }, - "components": { - "messages": { - "test:_:Handle:Message": { - "title": "test:_:Handle:Message", - "correlationId": { - "location": "$message.header#/correlation_id" - }, - "payload": {"$ref": "#/components/schemas/EmptyPayload"}, - } - }, - "schemas": { - "EmptyPayload": {"title": "EmptyPayload", "type": "null"} - }, - }, - } - ) diff --git a/tests/asyncapi/rabbit/test_publisher.py b/tests/asyncapi/rabbit/test_publisher.py deleted file mode 100644 index bbe4faf3c8..0000000000 --- a/tests/asyncapi/rabbit/test_publisher.py +++ /dev/null @@ -1,188 +0,0 @@ -from faststream.asyncapi.generate import get_app_schema -from faststream.rabbit import ExchangeType, RabbitBroker, RabbitExchange, RabbitQueue -from tests.asyncapi.base.publisher import PublisherTestcase - - -class TestArguments(PublisherTestcase): - broker_class = RabbitBroker - - def test_just_exchange(self): - broker = self.broker_class("amqp://guest:guest@localhost:5672/vhost") - - @broker.publisher(exchange="test-ex") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - assert schema["channels"] == { - "_:test-ex:Publisher": { - "bindings": { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": { - "autoDelete": False, - "durable": False, - "name": "test-ex", - "type": "direct", - "vhost": "/vhost", - }, - "is": "routingKey", - } - }, - "publish": { - "bindings": { - "amqp": { - "ack": True, - "bindingVersion": "0.2.0", - "deliveryMode": 1, - "mandatory": True, - } - }, - "message": { - "$ref": "#/components/messages/_:test-ex:Publisher:Message" - }, - }, - "servers": ["development"], - } - }, schema["channels"] - - def test_publisher_bindings(self): - broker = self.broker_class() - - @broker.publisher( - RabbitQueue("test", auto_delete=True), - RabbitExchange("test-ex", type=ExchangeType.TOPIC), - ) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": { - "autoDelete": False, - "durable": False, - "name": "test-ex", - "type": "topic", - "vhost": "/", - }, - "is": "routingKey", - "queue": { - "autoDelete": True, - "durable": False, - "exclusive": False, - "name": "test", - "vhost": "/", - }, - } - } - - def test_useless_queue_bindings(self): - broker = self.broker_class() - - @broker.publisher( - RabbitQueue("test", auto_delete=True), - RabbitExchange("test-ex", type=ExchangeType.FANOUT), - ) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - assert schema["channels"] == { - "_:test-ex:Publisher": { - "bindings": { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": { - "autoDelete": False, - "durable": False, - "name": "test-ex", - "type": "fanout", - "vhost": "/", - }, - "is": "routingKey", - } - }, - "publish": { - "message": { - "$ref": "#/components/messages/_:test-ex:Publisher:Message" - } - }, - "servers": ["development"], - } - } - - def test_reusable_exchange(self): - broker = self.broker_class("amqp://guest:guest@localhost:5672/vhost") - - @broker.publisher(exchange="test-ex", routing_key="key1") - @broker.publisher(exchange="test-ex", routing_key="key2", priority=10) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - - assert schema["channels"] == { - "key1:test-ex:Publisher": { - "bindings": { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": { - "autoDelete": False, - "durable": False, - "name": "test-ex", - "type": "direct", - "vhost": "/vhost", - }, - "is": "routingKey", - } - }, - "publish": { - "bindings": { - "amqp": { - "ack": True, - "bindingVersion": "0.2.0", - "cc": "key1", - "deliveryMode": 1, - "mandatory": True, - } - }, - "message": { - "$ref": "#/components/messages/key1:test-ex:Publisher:Message" - }, - }, - "servers": ["development"], - }, - "key2:test-ex:Publisher": { - "bindings": { - "amqp": { - "bindingVersion": "0.2.0", - "exchange": { - "autoDelete": False, - "durable": False, - "name": "test-ex", - "type": "direct", - "vhost": "/vhost", - }, - "is": "routingKey", - } - }, - "publish": { - "bindings": { - "amqp": { - "ack": True, - "bindingVersion": "0.2.0", - "cc": "key2", - "deliveryMode": 1, - "priority": 10, - "mandatory": True, - } - }, - "message": { - "$ref": "#/components/messages/key2:test-ex:Publisher:Message" - }, - }, - "servers": ["development"], - }, - } diff --git a/tests/asyncapi/rabbit/test_router.py b/tests/asyncapi/rabbit/test_router.py deleted file mode 100644 index 386f4960f5..0000000000 --- a/tests/asyncapi/rabbit/test_router.py +++ /dev/null @@ -1,112 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.rabbit import ( - RabbitBroker, - RabbitPublisher, - RabbitQueue, - RabbitRoute, - RabbitRouter, -) -from tests.asyncapi.base.arguments import ArgumentsTestcase -from tests.asyncapi.base.publisher import PublisherTestcase -from tests.asyncapi.base.router import RouterTestcase - - -class TestRouter(RouterTestcase): - broker_class = RabbitBroker - router_class = RabbitRouter - route_class = RabbitRoute - publisher_class = RabbitPublisher - - def test_prefix(self): - broker = self.broker_class() - - router = self.router_class(prefix="test_") - - @router.subscriber(RabbitQueue("test", routing_key="key")) - async def handle(msg): ... - - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert ( - schema - == { - "asyncapi": "2.6.0", - "defaultContentType": "application/json", - "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, - "servers": { - "development": { - "url": "amqp://guest:guest@localhost:5672/", # pragma: allowlist secret - "protocol": "amqp", - "protocolVersion": "0.9.1", - } - }, - "channels": { - "test_test:_:Handle": { - "servers": ["development"], - "bindings": { - "amqp": { - "is": "routingKey", - "bindingVersion": "0.2.0", - "queue": { - "name": "test_test", - "durable": False, - "exclusive": False, - "autoDelete": False, - "vhost": "/", - }, - "exchange": {"type": "default", "vhost": "/"}, - } - }, - "subscribe": { - "bindings": { - "amqp": { - "cc": "test_key", - "ack": True, - "bindingVersion": "0.2.0", - } - }, - "message": { - "$ref": "#/components/messages/test_test:_:Handle:Message" - }, - }, - } - }, - "components": { - "messages": { - "test_test:_:Handle:Message": { - "title": "test_test:_:Handle:Message", - "correlationId": { - "location": "$message.header#/correlation_id" - }, - "payload": { - "$ref": "#/components/schemas/Handle:Message:Payload" - }, - } - }, - "schemas": { - "Handle:Message:Payload": {"title": "Handle:Message:Payload"} - }, - }, - } - ), schema - - -class TestRouterArguments(ArgumentsTestcase): - broker_class = RabbitRouter - - def build_app(self, router): - broker = RabbitBroker() - broker.include_router(router) - return FastStream(broker) - - -class TestRouterPublisher(PublisherTestcase): - broker_class = RabbitRouter - - def build_app(self, router): - broker = RabbitBroker() - broker.include_router(router) - return FastStream(broker) diff --git a/tests/asyncapi/rabbit/test_security.py b/tests/asyncapi/rabbit/test_security.py deleted file mode 100644 index 88ea3f683c..0000000000 --- a/tests/asyncapi/rabbit/test_security.py +++ /dev/null @@ -1,119 +0,0 @@ -import ssl - -from faststream.app import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.rabbit import RabbitBroker -from faststream.security import ( - BaseSecurity, - SASLPlaintext, -) - - -def test_base_security_schema(): - ssl_context = ssl.create_default_context() - security = BaseSecurity(ssl_context=ssl_context) - - broker = RabbitBroker("amqp://guest:guest@localhost:5672/", security=security) - - assert ( - broker.url == "amqps://guest:guest@localhost:5672/" # pragma: allowlist secret - ) # pragma: allowlist secret - assert broker._connection_kwargs.get("ssl_context") is ssl_context - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}, "securitySchemes": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "amqps", - "protocolVersion": "0.9.1", - "security": [], - "url": "amqps://guest:guest@localhost:5672/", # pragma: allowlist secret - } - }, - } - - -def test_plaintext_security_schema(): - ssl_context = ssl.create_default_context() - - security = SASLPlaintext( - ssl_context=ssl_context, - username="admin", - password="password", # pragma: allowlist secret - ) - - broker = RabbitBroker("amqp://guest:guest@localhost/", security=security) - - assert ( - broker.url - == "amqps://admin:password@localhost:5671/" # pragma: allowlist secret - ) # pragma: allowlist secret - assert broker._connection_kwargs.get("ssl_context") is ssl_context - - schema = get_app_schema(FastStream(broker)).to_jsonable() - assert ( - schema - == { - "asyncapi": "2.6.0", - "channels": {}, - "components": { - "messages": {}, - "schemas": {}, - "securitySchemes": {"user-password": {"type": "userPassword"}}, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "amqps", - "protocolVersion": "0.9.1", - "security": [{"user-password": []}], - "url": "amqps://admin:password@localhost:5671/", # pragma: allowlist secret - } - }, - } - ) - - -def test_plaintext_security_schema_without_ssl(): - security = SASLPlaintext( - username="admin", - password="password", # pragma: allowlist secret - ) - - broker = RabbitBroker("amqp://guest:guest@localhost:5672/", security=security) - - assert ( - broker.url - == "amqp://admin:password@localhost:5672/" # pragma: allowlist secret - ) # pragma: allowlist secret - - schema = get_app_schema(FastStream(broker)).to_jsonable() - assert ( - schema - == { - "asyncapi": "2.6.0", - "channels": {}, - "components": { - "messages": {}, - "schemas": {}, - "securitySchemes": {"user-password": {"type": "userPassword"}}, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "amqp", - "protocolVersion": "0.9.1", - "security": [{"user-password": []}], - "url": "amqp://admin:password@localhost:5672/", # pragma: allowlist secret - } - }, - } - ) diff --git a/tests/a_docs/getting_started/cli/rabbit/__init__.py b/tests/asyncapi/rabbit/v2_6_0/__init__.py similarity index 100% rename from tests/a_docs/getting_started/cli/rabbit/__init__.py rename to tests/asyncapi/rabbit/v2_6_0/__init__.py diff --git a/tests/asyncapi/rabbit/v2_6_0/test_arguments.py b/tests/asyncapi/rabbit/v2_6_0/test_arguments.py new file mode 100644 index 0000000000..a8e2c4f745 --- /dev/null +++ b/tests/asyncapi/rabbit/v2_6_0/test_arguments.py @@ -0,0 +1,170 @@ +from faststream.rabbit import ExchangeType, RabbitBroker, RabbitExchange, RabbitQueue +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase + + +class TestArguments(ArgumentsTestcase): + broker_class = RabbitBroker + + def test_subscriber_bindings(self) -> None: + broker = self.broker_class() + + @broker.subscriber( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.TOPIC), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "topic", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": True, + "durable": False, + "exclusive": False, + "name": "test", + "vhost": "/", + }, + }, + } + + def test_subscriber_fanout_bindings(self) -> None: + broker = self.broker_class() + + @broker.subscriber( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.FANOUT), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + }, + } + + def test_subscriber_headers_bindings(self) -> None: + broker = self.broker_class() + + @broker.subscriber( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.HEADERS), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "headers", + "vhost": "/", + }, + "is": "routingKey", + }, + } + + def test_subscriber_xdelay_bindings(self) -> None: + broker = self.broker_class() + + @broker.subscriber( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.X_DELAYED_MESSAGE), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "x-delayed-message", + "vhost": "/", + }, + "is": "routingKey", + }, + } + + def test_subscriber_consistent_hash_bindings(self) -> None: + broker = self.broker_class() + + @broker.subscriber( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.X_CONSISTENT_HASH), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "x-consistent-hash", + "vhost": "/", + }, + "is": "routingKey", + }, + } + + def test_subscriber_modules_hash_bindings(self) -> None: + broker = self.broker_class() + + @broker.subscriber( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.X_MODULUS_HASH), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "x-modulus-hash", + "vhost": "/", + }, + "is": "routingKey", + }, + } diff --git a/tests/asyncapi/rabbit/v2_6_0/test_connection.py b/tests/asyncapi/rabbit/v2_6_0/test_connection.py new file mode 100644 index 0000000000..7a7a98994e --- /dev/null +++ b/tests/asyncapi/rabbit/v2_6_0/test_connection.py @@ -0,0 +1,120 @@ +from faststream.rabbit import RabbitBroker +from faststream.specification import Tag +from faststream.specification.asyncapi import AsyncAPI + + +def test_base() -> None: + schema = AsyncAPI( + RabbitBroker( + "amqps://localhost", + port=5673, + protocol_version="0.9.0", + description="Test description", + tags=(Tag(name="some-tag", description="experimental"),), + ), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "description": "Test description", + "protocol": "amqps", + "protocolVersion": "0.9.0", + "tags": [{"description": "experimental", "name": "some-tag"}], + "url": "amqps://guest:guest@localhost:5673/", # pragma: allowlist secret + }, + }, + } + + +def test_kwargs() -> None: + broker = RabbitBroker( + "amqp://guest:guest@localhost:5672/?heartbeat=300", # pragma: allowlist secret + host="127.0.0.1", + ) + + assert ( + broker.specification.url + == [ + "amqp://guest:guest@127.0.0.1:5672/?heartbeat=300" + ] # pragma: allowlist secret + ) + + +def test_custom() -> None: + broker = RabbitBroker( + "amqps://localhost", + specification_url="amqp://guest:guest@127.0.0.1:5672/vh", # pragma: allowlist secret + ) + + broker.publisher("test") + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert ( + schema + == { + "asyncapi": "2.6.0", + "channels": { + "test:_:Publisher": { + "bindings": { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": {"type": "default", "vhost": "/vh"}, + "is": "routingKey", + "queue": { + "autoDelete": False, + "durable": False, + "exclusive": False, + "name": "test", + "vhost": "/vh", + }, + }, + }, + "subscribe": { + "bindings": { + "amqp": { + "ack": True, + "bindingVersion": "0.2.0", + "cc": "test", + "deliveryMode": 1, + "mandatory": True, + }, + }, + "message": { + "$ref": "#/components/messages/test:_:Publisher:Message", + }, + }, + "servers": ["development"], + }, + }, + "components": { + "messages": { + "test:_:Publisher:Message": { + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": { + "$ref": "#/components/schemas/test:_:PublisherPayload", + }, + "title": "test:_:Publisher:Message", + }, + }, + "schemas": {"test:_:PublisherPayload": {}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "amqp", + "protocolVersion": "0.9.1", + "url": "amqp://guest:guest@127.0.0.1:5672/vh", # pragma: allowlist secret + }, + }, + } + ), schema diff --git a/tests/asyncapi/rabbit/v2_6_0/test_fastapi.py b/tests/asyncapi/rabbit/v2_6_0/test_fastapi.py new file mode 100644 index 0000000000..29e46b3078 --- /dev/null +++ b/tests/asyncapi/rabbit/v2_6_0/test_fastapi.py @@ -0,0 +1,41 @@ +from faststream.rabbit.fastapi import RabbitRouter +from faststream.rabbit.testing import TestRabbitBroker +from faststream.security import SASLPlaintext +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import FastAPICompatible +from tests.asyncapi.base.v2_6_0.fastapi import FastAPITestCase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase + + +class TestRouterArguments(FastAPITestCase, FastAPICompatible): + broker_class = staticmethod(lambda: RabbitRouter().broker) + router_class = RabbitRouter + broker_wrapper = staticmethod(TestRabbitBroker) + + def build_app(self, router): + return router + + +class TestRouterPublisher(PublisherTestcase): + broker_class = staticmethod(lambda: RabbitRouter().broker) + + def build_app(self, router): + return router + + +def test_fastapi_security_schema() -> None: + security = SASLPlaintext(username="user", password="pass", use_ssl=False) + + router = RabbitRouter(security=security) + + schema = AsyncAPI(router.broker, schema_version="2.6.0").to_jsonable() + + assert schema["servers"]["development"] == { + "protocol": "amqp", + "protocolVersion": "0.9.1", + "security": [{"user-password": []}], + "url": "amqp://user:pass@localhost:5672/", # pragma: allowlist secret + } + assert schema["components"]["securitySchemes"] == { + "user-password": {"type": "userPassword"}, + } diff --git a/tests/asyncapi/rabbit/v2_6_0/test_naming.py b/tests/asyncapi/rabbit/v2_6_0/test_naming.py new file mode 100644 index 0000000000..2ee937f21e --- /dev/null +++ b/tests/asyncapi/rabbit/v2_6_0/test_naming.py @@ -0,0 +1,104 @@ +from faststream.rabbit import RabbitBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.naming import NamingTestCase + + +class TestNaming(NamingTestCase): + broker_class: type[RabbitBroker] = RabbitBroker + + def test_subscriber_with_exchange(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test", "exchange") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["test:exchange:Handle"] + + assert list(schema["components"]["messages"].keys()) == [ + "test:exchange:Handle:Message", + ] + + def test_publisher_with_exchange(self) -> None: + broker = self.broker_class() + + @broker.publisher("test", "exchange") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert list(schema["channels"].keys()) == ["test:exchange:Publisher"] + + assert list(schema["components"]["messages"].keys()) == [ + "test:exchange:Publisher:Message", + ] + + def test_base(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert ( + schema + == { + "asyncapi": "2.6.0", + "defaultContentType": "application/json", + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "servers": { + "development": { + "url": "amqp://guest:guest@localhost:5672/", # pragma: allowlist secret + "protocol": "amqp", + "protocolVersion": "0.9.1", + }, + }, + "channels": { + "test:_:Handle": { + "servers": ["development"], + "bindings": { + "amqp": { + "is": "routingKey", + "bindingVersion": "0.2.0", + "queue": { + "name": "test", + "durable": False, + "exclusive": False, + "autoDelete": False, + "vhost": "/", + }, + "exchange": {"type": "default", "vhost": "/"}, + }, + }, + "publish": { + "bindings": { + "amqp": { + "cc": "test", + "ack": True, + "bindingVersion": "0.2.0", + }, + }, + "message": { + "$ref": "#/components/messages/test:_:Handle:Message", + }, + }, + }, + }, + "components": { + "messages": { + "test:_:Handle:Message": { + "title": "test:_:Handle:Message", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/EmptyPayload"}, + }, + }, + "schemas": { + "EmptyPayload": {"title": "EmptyPayload", "type": "null"}, + }, + }, + } + ) diff --git a/tests/asyncapi/rabbit/v2_6_0/test_publisher.py b/tests/asyncapi/rabbit/v2_6_0/test_publisher.py new file mode 100644 index 0000000000..e24edd3fed --- /dev/null +++ b/tests/asyncapi/rabbit/v2_6_0/test_publisher.py @@ -0,0 +1,196 @@ +from faststream.rabbit import ExchangeType, RabbitBroker, RabbitExchange, RabbitQueue +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase + + +class TestArguments(PublisherTestcase): + broker_class = RabbitBroker + + def test_just_exchange(self) -> None: + broker = self.broker_class("amqp://guest:guest@localhost:5672/vhost") + + @broker.publisher(exchange="test-ex") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + assert schema["channels"] == { + "_:test-ex:Publisher": { + "bindings": { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "direct", + "vhost": "/vhost", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "bindings": { + "amqp": { + "ack": True, + "bindingVersion": "0.2.0", + "deliveryMode": 1, + "mandatory": True, + }, + }, + "message": { + "$ref": "#/components/messages/_:test-ex:Publisher:Message", + }, + }, + "servers": ["development"], + }, + }, schema["channels"] + + def test_publisher_bindings(self) -> None: + broker = self.broker_class() + + @broker.publisher( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.TOPIC), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "topic", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": True, + "durable": False, + "exclusive": False, + "name": "test", + "vhost": "/", + }, + }, + } + + def test_useless_queue_bindings(self) -> None: + broker = self.broker_class() + + @broker.publisher( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.FANOUT), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + assert schema["channels"] == { + "_:test-ex:Publisher": { + "bindings": { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "bindings": { + "amqp": { + "ack": True, + "bindingVersion": "0.2.0", + "deliveryMode": 1, + "mandatory": True, + }, + }, + "message": { + "$ref": "#/components/messages/_:test-ex:Publisher:Message", + }, + }, + "servers": ["development"], + }, + } + + def test_reusable_exchange(self) -> None: + broker = self.broker_class("amqp://guest:guest@localhost:5672/vhost") + + @broker.publisher(exchange="test-ex", routing_key="key1") + @broker.publisher(exchange="test-ex", routing_key="key2", priority=10) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + + assert schema["channels"] == { + "key1:test-ex:Publisher": { + "bindings": { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "direct", + "vhost": "/vhost", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "bindings": { + "amqp": { + "ack": True, + "bindingVersion": "0.2.0", + "cc": "key1", + "deliveryMode": 1, + "mandatory": True, + }, + }, + "message": { + "$ref": "#/components/messages/key1:test-ex:Publisher:Message", + }, + }, + "servers": ["development"], + }, + "key2:test-ex:Publisher": { + "bindings": { + "amqp": { + "bindingVersion": "0.2.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "direct", + "vhost": "/vhost", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "bindings": { + "amqp": { + "ack": True, + "bindingVersion": "0.2.0", + "cc": "key2", + "deliveryMode": 1, + "priority": 10, + "mandatory": True, + }, + }, + "message": { + "$ref": "#/components/messages/key2:test-ex:Publisher:Message", + }, + }, + "servers": ["development"], + }, + }, schema["channels"] diff --git a/tests/asyncapi/rabbit/v2_6_0/test_router.py b/tests/asyncapi/rabbit/v2_6_0/test_router.py new file mode 100644 index 0000000000..4af00d1707 --- /dev/null +++ b/tests/asyncapi/rabbit/v2_6_0/test_router.py @@ -0,0 +1,82 @@ +from dirty_equals import IsPartialDict + +from faststream.rabbit import ( + RabbitBroker, + RabbitPublisher, + RabbitQueue, + RabbitRoute, + RabbitRouter, +) +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase +from tests.asyncapi.base.v2_6_0.router import RouterTestcase + + +class TestRouter(RouterTestcase): + broker_class = RabbitBroker + router_class = RabbitRouter + route_class = RabbitRoute + publisher_class = RabbitPublisher + + def test_prefix(self) -> None: + broker = self.broker_class() + + router = self.router_class(prefix="test_") + + @router.subscriber(RabbitQueue("test", routing_key="key")) + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema["channels"] == IsPartialDict({ + "test_test:_:Handle": { + "servers": ["development"], + "bindings": { + "amqp": { + "is": "routingKey", + "bindingVersion": "0.2.0", + "queue": { + "name": "test_test", + "durable": False, + "exclusive": False, + "autoDelete": False, + "vhost": "/", + }, + "exchange": {"type": "default", "vhost": "/"}, + }, + }, + "publish": { + "bindings": { + "amqp": { + "cc": "test_key", + "ack": True, + "bindingVersion": "0.2.0", + }, + }, + "message": { + "$ref": "#/components/messages/test_test:_:Handle:Message", + }, + }, + }, + }), schema["channels"] + + +class TestRouterArguments(ArgumentsTestcase): + broker_class = RabbitRouter + + def build_app(self, router): + broker = RabbitBroker() + broker.include_router(router) + return broker + + +class TestRouterPublisher(PublisherTestcase): + broker_class = RabbitRouter + + def build_app(self, router): + broker = RabbitBroker() + broker.include_router(router) + return broker diff --git a/tests/asyncapi/rabbit/v2_6_0/test_security.py b/tests/asyncapi/rabbit/v2_6_0/test_security.py new file mode 100644 index 0000000000..e07301269e --- /dev/null +++ b/tests/asyncapi/rabbit/v2_6_0/test_security.py @@ -0,0 +1,119 @@ +import ssl + +from faststream.rabbit import RabbitBroker +from faststream.security import ( + BaseSecurity, + SASLPlaintext, +) +from faststream.specification.asyncapi import AsyncAPI + + +def test_base_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = BaseSecurity(ssl_context=ssl_context) + + broker = RabbitBroker("amqp://guest:guest@localhost:5672/", security=security) + + assert ( + broker.specification.url + == ["amqps://guest:guest@localhost:5672/"] # pragma: allowlist secret + ) # pragma: allowlist secret + assert broker._connection_kwargs.get("ssl_context") is ssl_context + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}, "securitySchemes": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "amqps", + "protocolVersion": "0.9.1", + "security": [], + "url": "amqps://guest:guest@localhost:5672/", # pragma: allowlist secret + }, + }, + } + + +def test_plaintext_security_schema() -> None: + ssl_context = ssl.create_default_context() + + security = SASLPlaintext( + ssl_context=ssl_context, + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = RabbitBroker("amqp://guest:guest@localhost/", security=security) + + assert ( + broker.specification.url + == ["amqps://admin:password@localhost:5671/"] # pragma: allowlist secret + ) # pragma: allowlist secret + assert broker._connection_kwargs.get("ssl_context") is ssl_context + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + assert ( + schema + == { + "asyncapi": "2.6.0", + "channels": {}, + "components": { + "messages": {}, + "schemas": {}, + "securitySchemes": {"user-password": {"type": "userPassword"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "amqps", + "protocolVersion": "0.9.1", + "security": [{"user-password": []}], + "url": "amqps://admin:password@localhost:5671/", # pragma: allowlist secret + }, + }, + } + ) + + +def test_plaintext_security_schema_without_ssl() -> None: + security = SASLPlaintext( + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = RabbitBroker("amqp://guest:guest@localhost:5672/", security=security) + + assert ( + broker.specification.url + == ["amqp://admin:password@localhost:5672/"] # pragma: allowlist secret + ) # pragma: allowlist secret + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + assert ( + schema + == { + "asyncapi": "2.6.0", + "channels": {}, + "components": { + "messages": {}, + "schemas": {}, + "securitySchemes": {"user-password": {"type": "userPassword"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "amqp", + "protocolVersion": "0.9.1", + "security": [{"user-password": []}], + "url": "amqp://admin:password@localhost:5672/", # pragma: allowlist secret + }, + }, + } + ) diff --git a/tests/a_docs/rabbit/__init__.py b/tests/asyncapi/rabbit/v3_0_0/__init__.py similarity index 100% rename from tests/a_docs/rabbit/__init__.py rename to tests/asyncapi/rabbit/v3_0_0/__init__.py diff --git a/tests/asyncapi/rabbit/v3_0_0/test_arguments.py b/tests/asyncapi/rabbit/v3_0_0/test_arguments.py new file mode 100644 index 0000000000..1b4ef1f730 --- /dev/null +++ b/tests/asyncapi/rabbit/v3_0_0/test_arguments.py @@ -0,0 +1,59 @@ +from faststream.rabbit import ExchangeType, RabbitBroker, RabbitExchange, RabbitQueue +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.arguments import ArgumentsTestcase + + +class TestArguments(ArgumentsTestcase): + broker_factory = RabbitBroker + + def test_subscriber_bindings(self) -> None: + broker = self.broker_factory() + + @broker.subscriber( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.TOPIC), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "amqp": { + "bindingVersion": "0.3.0", + "is": "queue", + "queue": { + "autoDelete": True, + "durable": False, + "exclusive": False, + "name": "test", + "vhost": "/", + }, + }, + } + + def test_subscriber_fanout_bindings(self) -> None: + broker = self.broker_factory() + + @broker.subscriber( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.FANOUT), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "amqp": { + "bindingVersion": "0.3.0", + "queue": { + "autoDelete": True, + "durable": False, + "exclusive": False, + "name": "test", + "vhost": "/", + }, + "is": "queue", + }, + } diff --git a/tests/asyncapi/rabbit/v3_0_0/test_connection.py b/tests/asyncapi/rabbit/v3_0_0/test_connection.py new file mode 100644 index 0000000000..1c2ad43c49 --- /dev/null +++ b/tests/asyncapi/rabbit/v3_0_0/test_connection.py @@ -0,0 +1,131 @@ +from faststream.rabbit import RabbitBroker +from faststream.specification import Tag +from faststream.specification.asyncapi import AsyncAPI + + +def test_base() -> None: + schema = AsyncAPI( + RabbitBroker( + "amqps://localhost", + port=5673, + protocol_version="0.9.0", + description="Test description", + tags=(Tag(name="some-tag", description="experimental"),), + ), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "description": "Test description", + "protocol": "amqps", + "protocolVersion": "0.9.0", + "tags": [{"description": "experimental", "name": "some-tag"}], + "host": "guest:guest@localhost:5673", # pragma: allowlist secret + "pathname": "/", + }, + }, + } + + +def test_kwargs() -> None: + broker = RabbitBroker( + "amqp://guest:guest@localhost:5672/?heartbeat=300", # pragma: allowlist secret + host="127.0.0.1", + ) + + assert ( + broker.specification.url + == [ + "amqp://guest:guest@127.0.0.1:5672/?heartbeat=300" + ] # pragma: allowlist secret + ) + + +def test_custom() -> None: + broker = RabbitBroker( + "amqps://localhost", + specification_url="amqp://guest:guest@127.0.0.1:5672/vh", # pragma: allowlist secret + ) + + broker.publisher("test") + schema = AsyncAPI(broker, schema_version="3.0.0").to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": { + "test:_:Publisher": { + "address": "test:_:Publisher", + "bindings": { + "amqp": { + "bindingVersion": "0.3.0", + "exchange": {"type": "default", "vhost": "/vh"}, + "is": "routingKey", + }, + }, + "servers": [ + { + "$ref": "#/servers/development", + }, + ], + "messages": { + "Message": { + "$ref": "#/components/messages/test:_:Publisher:Message", + }, + }, + }, + }, + "operations": { + "test:_:Publisher": { + "action": "send", + "bindings": { + "amqp": { + "ack": True, + "bindingVersion": "0.3.0", + "cc": [ + "test", + ], + "deliveryMode": 1, + "mandatory": True, + }, + }, + "channel": { + "$ref": "#/channels/test:_:Publisher", + }, + "messages": [ + { + "$ref": "#/channels/test:_:Publisher/messages/Message", + }, + ], + }, + }, + "components": { + "messages": { + "test:_:Publisher:Message": { + "correlationId": {"location": "$message.header#/correlation_id"}, + "payload": { + "$ref": "#/components/schemas/test:_:Publisher:Message:Payload", + }, + "title": "test:_:Publisher:Message", + }, + }, + "schemas": {"test:_:Publisher:Message:Payload": {}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "amqp", + "protocolVersion": "0.9.1", + "host": "guest:guest@127.0.0.1:5672", # pragma: allowlist secret + "pathname": "/vh", # pragma: allowlist secret + }, + }, + } diff --git a/tests/asyncapi/rabbit/v3_0_0/test_fastapi.py b/tests/asyncapi/rabbit/v3_0_0/test_fastapi.py new file mode 100644 index 0000000000..6bbd0d172a --- /dev/null +++ b/tests/asyncapi/rabbit/v3_0_0/test_fastapi.py @@ -0,0 +1,42 @@ +from faststream.rabbit.fastapi import RabbitRouter +from faststream.rabbit.testing import TestRabbitBroker +from faststream.security import SASLPlaintext +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.arguments import FastAPICompatible +from tests.asyncapi.base.v3_0_0.fastapi import FastAPITestCase +from tests.asyncapi.base.v3_0_0.publisher import PublisherTestcase + + +class TestRouterArguments(FastAPITestCase, FastAPICompatible): + broker_factory = staticmethod(lambda: RabbitRouter().broker) + router_factory = RabbitRouter + broker_wrapper = staticmethod(TestRabbitBroker) + + def build_app(self, router): + return router + + +class TestRouterPublisher(PublisherTestcase): + broker_factory = staticmethod(lambda: RabbitRouter().broker) + + def build_app(self, router): + return router + + +def test_fastapi_security_schema() -> None: + security = SASLPlaintext(username="user", password="pass", use_ssl=False) + + router = RabbitRouter(security=security) + + schema = AsyncAPI(router.broker, schema_version="3.0.0").to_jsonable() + + assert schema["servers"]["development"] == { + "protocol": "amqp", + "protocolVersion": "0.9.1", + "security": [{"user-password": []}], + "host": "user:pass@localhost:5672", + "pathname": "/", + } + assert schema["components"]["securitySchemes"] == { + "user-password": {"type": "userPassword"}, + } diff --git a/tests/asyncapi/rabbit/v3_0_0/test_naming.py b/tests/asyncapi/rabbit/v3_0_0/test_naming.py new file mode 100644 index 0000000000..2839a4505a --- /dev/null +++ b/tests/asyncapi/rabbit/v3_0_0/test_naming.py @@ -0,0 +1,128 @@ +from faststream.rabbit import RabbitBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.naming import NamingTestCase + + +class TestNaming(NamingTestCase): + broker_class: type[RabbitBroker] = RabbitBroker + + def test_subscriber_with_exchange(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test", "exchange") + async def handle() -> None: ... + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + + assert list(schema["channels"].keys()) == ["test:exchange:Handle"] + + assert list(schema["components"]["messages"].keys()) == [ + "test:exchange:Handle:SubscribeMessage", + ] + + def test_publisher_with_exchange(self) -> None: + broker = self.broker_class() + + @broker.publisher("test", "exchange") + async def handle() -> None: ... + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + + assert list(schema["channels"].keys()) == ["test:exchange:Publisher"] + + assert list(schema["components"]["messages"].keys()) == [ + "test:exchange:Publisher:Message", + ] + + def test_base(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "defaultContentType": "application/json", + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "servers": { + "development": { + "host": "guest:guest@localhost:5672", # pragma: allowlist secret + "pathname": "/", + "protocol": "amqp", + "protocolVersion": "0.9.1", + }, + }, + "channels": { + "test:_:Handle": { + "address": "test:_:Handle", + "servers": [ + { + "$ref": "#/servers/development", + }, + ], + "bindings": { + "amqp": { + "is": "queue", + "bindingVersion": "0.3.0", + "queue": { + "name": "test", + "durable": False, + "exclusive": False, + "autoDelete": False, + "vhost": "/", + }, + }, + }, + "messages": { + "SubscribeMessage": { + "$ref": "#/components/messages/test:_:Handle:SubscribeMessage", + }, + }, + }, + }, + "operations": { + "test:_:HandleSubscribe": { + "action": "receive", + "bindings": { + "amqp": { + "ack": True, + "bindingVersion": "0.3.0", + "cc": [ + "test", + ], + }, + }, + "channel": { + "$ref": "#/channels/test:_:Handle", + }, + "messages": [ + { + "$ref": "#/channels/test:_:Handle/messages/SubscribeMessage", + }, + ], + }, + }, + "components": { + "messages": { + "test:_:Handle:SubscribeMessage": { + "title": "test:_:Handle:SubscribeMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/EmptyPayload"}, + }, + }, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + }, + } diff --git a/tests/asyncapi/rabbit/v3_0_0/test_publisher.py b/tests/asyncapi/rabbit/v3_0_0/test_publisher.py new file mode 100644 index 0000000000..b4826fe7e0 --- /dev/null +++ b/tests/asyncapi/rabbit/v3_0_0/test_publisher.py @@ -0,0 +1,258 @@ +from faststream.rabbit import ExchangeType, RabbitBroker, RabbitExchange, RabbitQueue +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.publisher import PublisherTestcase + + +class TestArguments(PublisherTestcase): + broker_factory = RabbitBroker + + def test_just_exchange(self) -> None: + broker = self.broker_factory("amqp://guest:guest@localhost:5672/vhost") + + @broker.publisher(exchange="test-ex") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + assert schema["channels"] == { + "_:test-ex:Publisher": { + "address": "_:test-ex:Publisher", + "bindings": { + "amqp": { + "bindingVersion": "0.3.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "direct", + "vhost": "/vhost", + }, + "is": "routingKey", + }, + }, + "servers": [ + { + "$ref": "#/servers/development", + }, + ], + "messages": { + "Message": { + "$ref": "#/components/messages/_:test-ex:Publisher:Message", + }, + }, + }, + }, schema["channels"] + + assert schema["operations"] == { + "_:test-ex:Publisher": { + "action": "send", + "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", + }, + ], + }, + } + + def test_publisher_bindings(self) -> None: + broker = self.broker_factory() + + @broker.publisher( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.TOPIC), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "amqp": { + "bindingVersion": "0.3.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "topic", + "vhost": "/", + }, + "is": "routingKey", + }, + } + + def test_useless_queue_bindings(self) -> None: + broker = self.broker_factory() + + @broker.publisher( + RabbitQueue("test", auto_delete=True), + RabbitExchange("test-ex", type=ExchangeType.FANOUT), + ) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + assert schema["channels"] == { + "_:test-ex:Publisher": { + "address": "_:test-ex:Publisher", + "bindings": { + "amqp": { + "bindingVersion": "0.3.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + }, + }, + "messages": { + "Message": { + "$ref": "#/components/messages/_:test-ex:Publisher:Message", + }, + }, + "servers": [ + { + "$ref": "#/servers/development", + }, + ], + }, + } + + assert schema["operations"] == { + "_:test-ex:Publisher": { + "action": "send", + "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"} + ], + } + } + + def test_reusable_exchange(self) -> None: + broker = self.broker_factory("amqp://guest:guest@localhost:5672/vhost") + + @broker.publisher(exchange="test-ex", routing_key="key1") + @broker.publisher(exchange="test-ex", routing_key="key2", priority=10) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + + assert schema["channels"] == { + "key1:test-ex:Publisher": { + "address": "key1:test-ex:Publisher", + "bindings": { + "amqp": { + "bindingVersion": "0.3.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "direct", + "vhost": "/vhost", + }, + "is": "routingKey", + }, + }, + "servers": [ + { + "$ref": "#/servers/development", + }, + ], + "messages": { + "Message": { + "$ref": "#/components/messages/key1:test-ex:Publisher:Message", + }, + }, + }, + "key2:test-ex:Publisher": { + "address": "key2:test-ex:Publisher", + "bindings": { + "amqp": { + "bindingVersion": "0.3.0", + "exchange": { + "autoDelete": False, + "durable": False, + "name": "test-ex", + "type": "direct", + "vhost": "/vhost", + }, + "is": "routingKey", + }, + }, + "servers": [ + { + "$ref": "#/servers/development", + }, + ], + "messages": { + "Message": { + "$ref": "#/components/messages/key2:test-ex:Publisher:Message", + }, + }, + }, + } + + assert schema["operations"] == { + "key1:test-ex:Publisher": { + "action": "send", + "channel": { + "$ref": "#/channels/key1:test-ex:Publisher", + }, + "bindings": { + "amqp": { + "ack": True, + "bindingVersion": "0.3.0", + "cc": [ + "key1", + ], + "deliveryMode": 1, + "mandatory": True, + }, + }, + "messages": [ + {"$ref": "#/channels/key1:test-ex:Publisher/messages/Message"}, + ], + }, + "key2:test-ex:Publisher": { + "action": "send", + "channel": { + "$ref": "#/channels/key2:test-ex:Publisher", + }, + "bindings": { + "amqp": { + "ack": True, + "bindingVersion": "0.3.0", + "cc": [ + "key2", + ], + "deliveryMode": 1, + "priority": 10, + "mandatory": True, + }, + }, + "messages": [ + {"$ref": "#/channels/key2:test-ex:Publisher/messages/Message"}, + ], + }, + } diff --git a/tests/asyncapi/rabbit/v3_0_0/test_router.py b/tests/asyncapi/rabbit/v3_0_0/test_router.py new file mode 100644 index 0000000000..e1bb277da6 --- /dev/null +++ b/tests/asyncapi/rabbit/v3_0_0/test_router.py @@ -0,0 +1,125 @@ +from faststream.rabbit import ( + RabbitBroker, + RabbitPublisher, + RabbitQueue, + RabbitRoute, + RabbitRouter, +) +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase +from tests.asyncapi.base.v3_0_0.router import RouterTestcase + + +class TestRouter(RouterTestcase): + broker_class = RabbitBroker + router_class = RabbitRouter + route_class = RabbitRoute + publisher_class = RabbitPublisher + + def test_prefix(self) -> None: + broker = self.broker_class() + + router = self.router_class(prefix="test_") + + @router.subscriber(RabbitQueue("test", routing_key="key")) + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "asyncapi": "3.0.0", + "defaultContentType": "application/json", + "servers": { + "development": { + "host": "guest:guest@localhost:5672", + "pathname": "/", + "protocol": "amqp", + "protocolVersion": "0.9.1", + }, + }, + "channels": { + "test_test:_:Handle": { + "address": "test_test:_:Handle", + "servers": [{"$ref": "#/servers/development"}], + "messages": { + "SubscribeMessage": { + "$ref": "#/components/messages/test_test:_:Handle:SubscribeMessage", + }, + }, + "bindings": { + "amqp": { + "is": "queue", + "bindingVersion": "0.3.0", + "queue": { + "name": "test_test", + "durable": False, + "exclusive": False, + "autoDelete": False, + "vhost": "/", + }, + }, + }, + }, + }, + "operations": { + "test_test:_:HandleSubscribe": { + "action": "receive", + "bindings": { + "amqp": { + "cc": [ + "test_key", + ], + "ack": True, + "bindingVersion": "0.3.0", + }, + }, + "messages": [ + { + "$ref": "#/channels/test_test:_:Handle/messages/SubscribeMessage", + }, + ], + "channel": {"$ref": "#/channels/test_test:_:Handle"}, + }, + }, + "components": { + "messages": { + "test_test:_:Handle:SubscribeMessage": { + "title": "test_test:_:Handle:SubscribeMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": { + "$ref": "#/components/schemas/Handle:Message:Payload", + }, + }, + }, + "schemas": { + "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + }, + }, + }, schema + + +class TestRouterArguments(ArgumentsTestcase): + broker_class = RabbitRouter + + def build_app(self, router): + broker = RabbitBroker() + broker.include_router(router) + return broker + + +class TestRouterPublisher(PublisherTestcase): + broker_class = RabbitRouter + + def build_app(self, router): + broker = RabbitBroker() + broker.include_router(router) + return broker diff --git a/tests/asyncapi/rabbit/v3_0_0/test_security.py b/tests/asyncapi/rabbit/v3_0_0/test_security.py new file mode 100644 index 0000000000..0dbc7329c9 --- /dev/null +++ b/tests/asyncapi/rabbit/v3_0_0/test_security.py @@ -0,0 +1,128 @@ +import ssl + +from faststream.rabbit import RabbitBroker +from faststream.security import ( + BaseSecurity, + SASLPlaintext, +) +from faststream.specification.asyncapi import AsyncAPI + + +def test_base_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = BaseSecurity(ssl_context=ssl_context) + + broker = RabbitBroker("amqp://guest:guest@localhost:5672/", security=security) + + assert ( + broker.specification.url + == ["amqps://guest:guest@localhost:5672/"] # pragma: allowlist secret + ) # pragma: allowlist secret + assert broker._connection_kwargs.get("ssl_context") is ssl_context + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}, "securitySchemes": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "amqps", + "protocolVersion": "0.9.1", + "security": [], + "host": "guest:guest@localhost:5672", # pragma: allowlist secret + "pathname": "/", + }, + }, + } + + +def test_plaintext_security_schema() -> None: + ssl_context = ssl.create_default_context() + + security = SASLPlaintext( + ssl_context=ssl_context, + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = RabbitBroker("amqp://guest:guest@localhost/", security=security) + + assert ( + broker.specification.url + == ["amqps://admin:password@localhost:5671/"] # pragma: allowlist secret + ) # pragma: allowlist secret + assert broker._connection_kwargs.get("ssl_context") is ssl_context + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": { + "messages": {}, + "schemas": {}, + "securitySchemes": {"user-password": {"type": "userPassword"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "amqps", + "protocolVersion": "0.9.1", + "security": [{"user-password": []}], + "host": "admin:password@localhost:5671", # pragma: allowlist secret + "pathname": "/", + }, + }, + } + + +def test_plaintext_security_schema_without_ssl() -> None: + security = SASLPlaintext( + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = RabbitBroker("amqp://guest:guest@localhost:5672/", security=security) + + assert ( + broker.specification.url + == ["amqp://admin:password@localhost:5672/"] # pragma: allowlist secret + ) # pragma: allowlist secret + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": { + "messages": {}, + "schemas": {}, + "securitySchemes": {"user-password": {"type": "userPassword"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "amqp", + "protocolVersion": "0.9.1", + "security": [{"user-password": []}], + "host": "admin:password@localhost:5672", # pragma: allowlist secret + "pathname": "/", # pragma: allowlist secret + }, + }, + } diff --git a/tests/asyncapi/redis/test_arguments.py b/tests/asyncapi/redis/test_arguments.py deleted file mode 100644 index 3f64aba9b6..0000000000 --- a/tests/asyncapi/redis/test_arguments.py +++ /dev/null @@ -1,86 +0,0 @@ -from faststream.asyncapi.generate import get_app_schema -from faststream.redis import RedisBroker, StreamSub -from tests.asyncapi.base.arguments import ArgumentsTestcase - - -class TestArguments(ArgumentsTestcase): - broker_class = RedisBroker - - def test_channel_subscriber(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "redis": { - "bindingVersion": "custom", - "channel": "test", - "method": "subscribe", - } - } - - def test_channel_pattern_subscriber(self): - broker = self.broker_class() - - @broker.subscriber("test.{path}") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "redis": { - "bindingVersion": "custom", - "channel": "test.*", - "method": "psubscribe", - } - } - - def test_list_subscriber(self): - broker = self.broker_class() - - @broker.subscriber(list="test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "redis": {"bindingVersion": "custom", "channel": "test", "method": "lpop"} - } - - def test_stream_subscriber(self): - broker = self.broker_class() - - @broker.subscriber(stream="test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "redis": {"bindingVersion": "custom", "channel": "test", "method": "xread"} - } - - def test_stream_group_subscriber(self): - broker = self.broker_class() - - @broker.subscriber(stream=StreamSub("test", group="group", consumer="consumer")) - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "redis": { - "bindingVersion": "custom", - "channel": "test", - "consumer_name": "consumer", - "group_name": "group", - "method": "xreadgroup", - } - } diff --git a/tests/asyncapi/redis/test_connection.py b/tests/asyncapi/redis/test_connection.py deleted file mode 100644 index a5719d4a77..0000000000 --- a/tests/asyncapi/redis/test_connection.py +++ /dev/null @@ -1,60 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.asyncapi.schema import Tag -from faststream.redis import RedisBroker - - -def test_base(): - schema = get_app_schema( - FastStream( - RedisBroker( - "redis://localhost:6379", - protocol="plaintext", - protocol_version="0.9.0", - description="Test description", - tags=(Tag(name="some-tag", description="experimental"),), - ) - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "description": "Test description", - "protocol": "plaintext", - "protocolVersion": "0.9.0", - "tags": [{"description": "experimental", "name": "some-tag"}], - "url": "redis://localhost:6379", - } - }, - }, schema - - -def test_custom(): - schema = get_app_schema( - FastStream( - RedisBroker( - "redis://localhost:6379", asyncapi_url="rediss://127.0.0.1:8000" - ) - ) - ).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "rediss", - "protocolVersion": "custom", - "url": "rediss://127.0.0.1:8000", - } - }, - } diff --git a/tests/asyncapi/redis/test_fastapi.py b/tests/asyncapi/redis/test_fastapi.py deleted file mode 100644 index 1a5466d4e8..0000000000 --- a/tests/asyncapi/redis/test_fastapi.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Type - -from faststream.redis import TestRedisBroker -from faststream.redis.fastapi import RedisRouter -from tests.asyncapi.base.arguments import FastAPICompatible -from tests.asyncapi.base.fastapi import FastAPITestCase -from tests.asyncapi.base.publisher import PublisherTestcase - - -class TestRouterArguments(FastAPITestCase, FastAPICompatible): - broker_class: Type[RedisRouter] = RedisRouter - broker_wrapper = staticmethod(TestRedisBroker) - - def build_app(self, router): - return router - - -class TestRouterPublisher(PublisherTestcase): - broker_class = RedisRouter - - def build_app(self, router): - return router diff --git a/tests/asyncapi/redis/test_naming.py b/tests/asyncapi/redis/test_naming.py deleted file mode 100644 index 92bcb5b0f9..0000000000 --- a/tests/asyncapi/redis/test_naming.py +++ /dev/null @@ -1,92 +0,0 @@ -import pytest - -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.redis import RedisBroker -from tests.asyncapi.base.naming import NamingTestCase - - -class TestNaming(NamingTestCase): - broker_class = RedisBroker - - def test_base(self): - broker = self.broker_class() - - @broker.subscriber("test") - async def handle(): ... - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": { - "test:Handle": { - "bindings": { - "redis": { - "bindingVersion": "custom", - "channel": "test", - "method": "subscribe", - } - }, - "servers": ["development"], - "subscribe": { - "message": {"$ref": "#/components/messages/test:Handle:Message"} - }, - } - }, - "components": { - "messages": { - "test:Handle:Message": { - "correlationId": { - "location": "$message.header#/correlation_id" - }, - "payload": {"$ref": "#/components/schemas/EmptyPayload"}, - "title": "test:Handle:Message", - } - }, - "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "redis", - "protocolVersion": "custom", - "url": "redis://localhost:6379", - } - }, - }, schema - - @pytest.mark.parametrize( - "args", - ( # noqa: PT007 - pytest.param({"channel": "test"}, id="channel"), - pytest.param({"list": "test"}, id="list"), - pytest.param({"stream": "test"}, id="stream"), - ), - ) - def test_subscribers_variations(self, args): - broker = self.broker_class() - - @broker.subscriber(**args) - async def handle(): ... - - schema = get_app_schema(FastStream(broker)) - assert list(schema.channels.keys()) == ["test:Handle"] - - @pytest.mark.parametrize( - "args", - ( # noqa: PT007 - pytest.param({"channel": "test"}, id="channel"), - pytest.param({"list": "test"}, id="list"), - pytest.param({"stream": "test"}, id="stream"), - ), - ) - def test_publisher_variations(self, args): - broker = self.broker_class() - - @broker.publisher(**args) - async def handle(): ... - - schema = get_app_schema(FastStream(broker)) - assert list(schema.channels.keys()) == ["test:Publisher"] diff --git a/tests/asyncapi/redis/test_publisher.py b/tests/asyncapi/redis/test_publisher.py deleted file mode 100644 index 8a82bca90d..0000000000 --- a/tests/asyncapi/redis/test_publisher.py +++ /dev/null @@ -1,50 +0,0 @@ -from faststream.asyncapi.generate import get_app_schema -from faststream.redis import RedisBroker -from tests.asyncapi.base.publisher import PublisherTestcase - - -class TestArguments(PublisherTestcase): - broker_class = RedisBroker - - def test_channel_publisher(self): - broker = self.broker_class() - - @broker.publisher("test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "redis": { - "bindingVersion": "custom", - "channel": "test", - "method": "publish", - } - } - - def test_list_publisher(self): - broker = self.broker_class() - - @broker.publisher(list="test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "redis": {"bindingVersion": "custom", "channel": "test", "method": "rpush"} - } - - def test_stream_publisher(self): - broker = self.broker_class() - - @broker.publisher(stream="test") - async def handle(msg): ... - - schema = get_app_schema(self.build_app(broker)).to_jsonable() - key = tuple(schema["channels"].keys())[0] # noqa: RUF015 - - assert schema["channels"][key]["bindings"] == { - "redis": {"bindingVersion": "custom", "channel": "test", "method": "xadd"} - } diff --git a/tests/asyncapi/redis/test_router.py b/tests/asyncapi/redis/test_router.py deleted file mode 100644 index eff7d40003..0000000000 --- a/tests/asyncapi/redis/test_router.py +++ /dev/null @@ -1,89 +0,0 @@ -from faststream import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.redis import RedisBroker, RedisPublisher, RedisRoute, RedisRouter -from tests.asyncapi.base.arguments import ArgumentsTestcase -from tests.asyncapi.base.publisher import PublisherTestcase -from tests.asyncapi.base.router import RouterTestcase - - -class TestRouter(RouterTestcase): - broker_class = RedisBroker - router_class = RedisRouter - route_class = RedisRoute - publisher_class = RedisPublisher - - def test_prefix(self): - broker = self.broker_class() - - router = self.router_class(prefix="test_") - - @router.subscriber("test") - async def handle(msg): ... - - broker.include_router(router) - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": { - "test_test:Handle": { - "bindings": { - "redis": { - "bindingVersion": "custom", - "channel": "test_test", - "method": "subscribe", - } - }, - "servers": ["development"], - "subscribe": { - "message": { - "$ref": "#/components/messages/test_test:Handle:Message" - } - }, - } - }, - "components": { - "messages": { - "test_test:Handle:Message": { - "correlationId": { - "location": "$message.header#/correlation_id" - }, - "payload": { - "$ref": "#/components/schemas/Handle:Message:Payload" - }, - "title": "test_test:Handle:Message", - } - }, - "schemas": { - "Handle:Message:Payload": {"title": "Handle:Message:Payload"} - }, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "redis", - "protocolVersion": "custom", - "url": "redis://localhost:6379", - } - }, - } - - -class TestRouterArguments(ArgumentsTestcase): - broker_class = RedisRouter - - def build_app(self, router): - broker = RedisBroker() - broker.include_router(router) - return FastStream(broker) - - -class TestRouterPublisher(PublisherTestcase): - broker_class = RedisRouter - - def build_app(self, router): - broker = RedisBroker() - broker.include_router(router) - return FastStream(broker) diff --git a/tests/asyncapi/redis/test_security.py b/tests/asyncapi/redis/test_security.py deleted file mode 100644 index b9ef40b41a..0000000000 --- a/tests/asyncapi/redis/test_security.py +++ /dev/null @@ -1,111 +0,0 @@ -import ssl - -from faststream.app import FastStream -from faststream.asyncapi.generate import get_app_schema -from faststream.redis import RedisBroker -from faststream.security import ( - BaseSecurity, - SASLPlaintext, -) - - -def test_base_security_schema(): - ssl_context = ssl.create_default_context() - security = BaseSecurity(ssl_context=ssl_context) - - broker = RedisBroker("rediss://localhost:6379/", security=security) - - assert ( - broker.url == "rediss://localhost:6379/" # pragma: allowlist secret - ) # pragma: allowlist secret - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": {"messages": {}, "schemas": {}, "securitySchemes": {}}, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "rediss", - "protocolVersion": "custom", - "security": [], - "url": "rediss://localhost:6379/", - } - }, - } - - -def test_plaintext_security_schema(): - ssl_context = ssl.create_default_context() - - security = SASLPlaintext( - ssl_context=ssl_context, - username="admin", - password="password", # pragma: allowlist secret - ) - - broker = RedisBroker("redis://localhost:6379/", security=security) - - assert ( - broker.url == "redis://localhost:6379/" # pragma: allowlist secret - ) # pragma: allowlist secret - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": { - "messages": {}, - "schemas": {}, - "securitySchemes": {"user-password": {"type": "userPassword"}}, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "redis", - "protocolVersion": "custom", - "security": [{"user-password": []}], - "url": "redis://localhost:6379/", - } - }, - } - - -def test_plaintext_security_schema_without_ssl(): - security = SASLPlaintext( - username="admin", - password="password", # pragma: allowlist secret - ) - - broker = RedisBroker("redis://localhost:6379/", security=security) - - assert ( - broker.url == "redis://localhost:6379/" # pragma: allowlist secret - ) # pragma: allowlist secret - - schema = get_app_schema(FastStream(broker)).to_jsonable() - - assert schema == { - "asyncapi": "2.6.0", - "channels": {}, - "components": { - "messages": {}, - "schemas": {}, - "securitySchemes": {"user-password": {"type": "userPassword"}}, - }, - "defaultContentType": "application/json", - "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, - "servers": { - "development": { - "protocol": "redis", - "protocolVersion": "custom", - "security": [{"user-password": []}], - "url": "redis://localhost:6379/", - } - }, - } diff --git a/tests/a_docs/getting_started/cli/redis/__init__.py b/tests/asyncapi/redis/v2_6_0/__init__.py similarity index 100% rename from tests/a_docs/getting_started/cli/redis/__init__.py rename to tests/asyncapi/redis/v2_6_0/__init__.py diff --git a/tests/asyncapi/redis/v2_6_0/test_arguments.py b/tests/asyncapi/redis/v2_6_0/test_arguments.py new file mode 100644 index 0000000000..403cccad84 --- /dev/null +++ b/tests/asyncapi/redis/v2_6_0/test_arguments.py @@ -0,0 +1,86 @@ +from faststream.redis import RedisBroker, StreamSub +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase + + +class TestArguments(ArgumentsTestcase): + broker_class = RedisBroker + + def test_channel_subscriber(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": { + "bindingVersion": "custom", + "channel": "test", + "method": "subscribe", + }, + } + + def test_channel_pattern_subscriber(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test.{path}") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": { + "bindingVersion": "custom", + "channel": "test.*", + "method": "psubscribe", + }, + } + + def test_list_subscriber(self) -> None: + broker = self.broker_class() + + @broker.subscriber(list="test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": {"bindingVersion": "custom", "channel": "test", "method": "lpop"}, + } + + def test_stream_subscriber(self) -> None: + broker = self.broker_class() + + @broker.subscriber(stream="test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": {"bindingVersion": "custom", "channel": "test", "method": "xread"}, + } + + def test_stream_group_subscriber(self) -> None: + broker = self.broker_class() + + @broker.subscriber(stream=StreamSub("test", group="group", consumer="consumer")) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": { + "bindingVersion": "custom", + "channel": "test", + "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 new file mode 100644 index 0000000000..194371e767 --- /dev/null +++ b/tests/asyncapi/redis/v2_6_0/test_connection.py @@ -0,0 +1,58 @@ +from faststream.redis import RedisBroker +from faststream.specification import Tag +from faststream.specification.asyncapi import AsyncAPI + + +def test_base() -> None: + schema = AsyncAPI( + RedisBroker( + "redis://localhost:6379", + protocol="plaintext", + protocol_version="0.9.0", + description="Test description", + tags=(Tag(name="some-tag", description="experimental"),), + ), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "description": "Test description", + "protocol": "plaintext", + "protocolVersion": "0.9.0", + "tags": [{"description": "experimental", "name": "some-tag"}], + "url": "redis://localhost:6379", + }, + }, + }, schema + + +def test_custom() -> None: + schema = AsyncAPI( + RedisBroker( + "redis://localhost:6379", + specification_url="rediss://127.0.0.1:8000", + ), + schema_version="2.6.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "rediss", + "protocolVersion": "custom", + "url": "rediss://127.0.0.1:8000", + }, + }, + } diff --git a/tests/asyncapi/redis/v2_6_0/test_fastapi.py b/tests/asyncapi/redis/v2_6_0/test_fastapi.py new file mode 100644 index 0000000000..3ccfff1c8c --- /dev/null +++ b/tests/asyncapi/redis/v2_6_0/test_fastapi.py @@ -0,0 +1,21 @@ +from faststream.redis import TestRedisBroker +from faststream.redis.fastapi import RedisRouter +from tests.asyncapi.base.v2_6_0.arguments import FastAPICompatible +from tests.asyncapi.base.v2_6_0.fastapi import FastAPITestCase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase + + +class TestRouterArguments(FastAPITestCase, FastAPICompatible): + broker_class = staticmethod(lambda: RedisRouter().broker) + router_class = RedisRouter + broker_wrapper = staticmethod(TestRedisBroker) + + def build_app(self, router): + return router + + +class TestRouterPublisher(PublisherTestcase): + broker_class = staticmethod(lambda: RedisRouter().broker) + + def build_app(self, router): + return router diff --git a/tests/asyncapi/redis/v2_6_0/test_naming.py b/tests/asyncapi/redis/v2_6_0/test_naming.py new file mode 100644 index 0000000000..e2558bb9a6 --- /dev/null +++ b/tests/asyncapi/redis/v2_6_0/test_naming.py @@ -0,0 +1,93 @@ +import pytest + +from faststream.redis import RedisBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.naming import NamingTestCase + + +class TestNaming(NamingTestCase): + broker_class = RedisBroker + + def test_base(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": { + "test:Handle": { + "bindings": { + "redis": { + "bindingVersion": "custom", + "channel": "test", + "method": "subscribe", + }, + }, + "servers": ["development"], + "publish": { + "message": { + "$ref": "#/components/messages/test:Handle:Message" + }, + }, + }, + }, + "components": { + "messages": { + "test:Handle:Message": { + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/EmptyPayload"}, + "title": "test:Handle:Message", + }, + }, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "redis", + "protocolVersion": "custom", + "url": "redis://localhost:6379", + }, + }, + }, schema + + @pytest.mark.parametrize( + "args", + ( + pytest.param({"channel": "test"}, id="channel"), + pytest.param({"list": "test"}, id="list"), + pytest.param({"stream": "test"}, id="stream"), + ), + ) + def test_subscribers_variations(self, args) -> None: + broker = self.broker_class() + + @broker.subscriber(**args) + async def handle() -> None: ... + + schema = AsyncAPI(broker) + assert list(schema.to_jsonable()["channels"].keys()) == ["test:Handle"] + + @pytest.mark.parametrize( + "args", + ( + pytest.param({"channel": "test"}, id="channel"), + pytest.param({"list": "test"}, id="list"), + pytest.param({"stream": "test"}, id="stream"), + ), + ) + def test_publisher_variations(self, args) -> None: + broker = self.broker_class() + + @broker.publisher(**args) + async def handle() -> None: ... + + schema = AsyncAPI(broker) + assert list(schema.to_jsonable()["channels"].keys()) == ["test:Publisher"] diff --git a/tests/asyncapi/redis/v2_6_0/test_publisher.py b/tests/asyncapi/redis/v2_6_0/test_publisher.py new file mode 100644 index 0000000000..939f79bd32 --- /dev/null +++ b/tests/asyncapi/redis/v2_6_0/test_publisher.py @@ -0,0 +1,50 @@ +from faststream.redis import RedisBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase + + +class TestArguments(PublisherTestcase): + broker_class = RedisBroker + + def test_channel_publisher(self) -> None: + broker = self.broker_class() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": { + "bindingVersion": "custom", + "channel": "test", + "method": "publish", + }, + } + + def test_list_publisher(self) -> None: + broker = self.broker_class() + + @broker.publisher(list="test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": {"bindingVersion": "custom", "channel": "test", "method": "rpush"}, + } + + def test_stream_publisher(self) -> None: + broker = self.broker_class() + + @broker.publisher(stream="test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="2.6.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": {"bindingVersion": "custom", "channel": "test", "method": "xadd"}, + } diff --git a/tests/asyncapi/redis/v2_6_0/test_router.py b/tests/asyncapi/redis/v2_6_0/test_router.py new file mode 100644 index 0000000000..7d37538dbc --- /dev/null +++ b/tests/asyncapi/redis/v2_6_0/test_router.py @@ -0,0 +1,88 @@ +from faststream.redis import RedisBroker, RedisPublisher, RedisRoute, RedisRouter +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase +from tests.asyncapi.base.v2_6_0.router import RouterTestcase + + +class TestRouter(RouterTestcase): + broker_class = RedisBroker + router_class = RedisRouter + route_class = RedisRoute + publisher_class = RedisPublisher + + def test_prefix(self) -> None: + broker = self.broker_class() + + router = self.router_class(prefix="test_") + + @router.subscriber("test") + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": { + "test_test:Handle": { + "bindings": { + "redis": { + "bindingVersion": "custom", + "channel": "test_test", + "method": "subscribe", + }, + }, + "servers": ["development"], + "publish": { + "message": { + "$ref": "#/components/messages/test_test:Handle:Message", + }, + }, + }, + }, + "components": { + "messages": { + "test_test:Handle:Message": { + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": { + "$ref": "#/components/schemas/Handle:Message:Payload", + }, + "title": "test_test:Handle:Message", + }, + }, + "schemas": { + "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + }, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "redis", + "protocolVersion": "custom", + "url": "redis://localhost:6379", + }, + }, + } + + +class TestRouterArguments(ArgumentsTestcase): + broker_class = RedisRouter + + def build_app(self, router): + broker = RedisBroker() + broker.include_router(router) + return broker + + +class TestRouterPublisher(PublisherTestcase): + broker_class = RedisRouter + + def build_app(self, router): + broker = RedisBroker() + broker.include_router(router) + return broker diff --git a/tests/asyncapi/redis/v2_6_0/test_security.py b/tests/asyncapi/redis/v2_6_0/test_security.py new file mode 100644 index 0000000000..8682906dd0 --- /dev/null +++ b/tests/asyncapi/redis/v2_6_0/test_security.py @@ -0,0 +1,113 @@ +import ssl + +from faststream.redis import RedisBroker +from faststream.security import ( + BaseSecurity, + SASLPlaintext, +) +from faststream.specification.asyncapi import AsyncAPI + + +def test_base_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = BaseSecurity(ssl_context=ssl_context) + + broker = RedisBroker("rediss://localhost:6379/", security=security) + + assert ( + broker.specification.url + == ["rediss://localhost:6379/"] # pragma: allowlist secret + ) # pragma: allowlist secret + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}, "securitySchemes": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "rediss", + "protocolVersion": "custom", + "security": [], + "url": "rediss://localhost:6379/", + }, + }, + } + + +def test_plaintext_security_schema() -> None: + ssl_context = ssl.create_default_context() + + security = SASLPlaintext( + ssl_context=ssl_context, + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = RedisBroker("redis://localhost:6379/", security=security) + + assert ( + broker.specification.url + == ["redis://localhost:6379/"] # pragma: allowlist secret + ) # pragma: allowlist secret + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": { + "messages": {}, + "schemas": {}, + "securitySchemes": {"user-password": {"type": "userPassword"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "redis", + "protocolVersion": "custom", + "security": [{"user-password": []}], + "url": "redis://localhost:6379/", + }, + }, + } + + +def test_plaintext_security_schema_without_ssl() -> None: + security = SASLPlaintext( + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = RedisBroker("redis://localhost:6379/", security=security) + + assert ( + broker.specification.url + == ["redis://localhost:6379/"] # pragma: allowlist secret + ) # pragma: allowlist secret + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": { + "messages": {}, + "schemas": {}, + "securitySchemes": {"user-password": {"type": "userPassword"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "redis", + "protocolVersion": "custom", + "security": [{"user-password": []}], + "url": "redis://localhost:6379/", + }, + }, + } diff --git a/tests/a_docs/redis/__init__.py b/tests/asyncapi/redis/v3_0_0/__init__.py similarity index 100% rename from tests/a_docs/redis/__init__.py rename to tests/asyncapi/redis/v3_0_0/__init__.py diff --git a/tests/asyncapi/redis/v3_0_0/test_arguments.py b/tests/asyncapi/redis/v3_0_0/test_arguments.py new file mode 100644 index 0000000000..0def5e4f41 --- /dev/null +++ b/tests/asyncapi/redis/v3_0_0/test_arguments.py @@ -0,0 +1,86 @@ +from faststream.redis import RedisBroker, StreamSub +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.arguments import ArgumentsTestcase + + +class TestArguments(ArgumentsTestcase): + broker_factory = RedisBroker + + def test_channel_subscriber(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": { + "bindingVersion": "custom", + "channel": "test", + "method": "subscribe", + }, + } + + def test_channel_pattern_subscriber(self) -> None: + broker = self.broker_factory() + + @broker.subscriber("test.{path}") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": { + "bindingVersion": "custom", + "channel": "test.*", + "method": "psubscribe", + }, + } + + def test_list_subscriber(self) -> None: + broker = self.broker_factory() + + @broker.subscriber(list="test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": {"bindingVersion": "custom", "channel": "test", "method": "lpop"}, + } + + def test_stream_subscriber(self) -> None: + broker = self.broker_factory() + + @broker.subscriber(stream="test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": {"bindingVersion": "custom", "channel": "test", "method": "xread"}, + } + + def test_stream_group_subscriber(self) -> None: + broker = self.broker_factory() + + @broker.subscriber(stream=StreamSub("test", group="group", consumer="consumer")) + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": { + "bindingVersion": "custom", + "channel": "test", + "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 new file mode 100644 index 0000000000..968e67b464 --- /dev/null +++ b/tests/asyncapi/redis/v3_0_0/test_connection.py @@ -0,0 +1,62 @@ +from faststream.redis import RedisBroker +from faststream.specification import Tag +from faststream.specification.asyncapi import AsyncAPI + + +def test_base() -> None: + schema = AsyncAPI( + RedisBroker( + "redis://localhost:6379", + protocol="plaintext", + protocol_version="0.9.0", + description="Test description", + tags=(Tag(name="some-tag", description="experimental"),), + ), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "description": "Test description", + "protocol": "plaintext", + "protocolVersion": "0.9.0", + "tags": [{"description": "experimental", "name": "some-tag"}], + "host": "localhost:6379", + "pathname": "", + }, + }, + }, schema + + +def test_custom() -> None: + schema = AsyncAPI( + RedisBroker( + "redis://localhost:6379", + specification_url="rediss://127.0.0.1:8000", + ), + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "rediss", + "protocolVersion": "custom", + "host": "127.0.0.1:8000", + "pathname": "", + }, + }, + } diff --git a/tests/asyncapi/redis/v3_0_0/test_fastapi.py b/tests/asyncapi/redis/v3_0_0/test_fastapi.py new file mode 100644 index 0000000000..fc75b0f092 --- /dev/null +++ b/tests/asyncapi/redis/v3_0_0/test_fastapi.py @@ -0,0 +1,21 @@ +from faststream.redis import TestRedisBroker +from faststream.redis.fastapi import RedisRouter +from tests.asyncapi.base.v3_0_0.arguments import FastAPICompatible +from tests.asyncapi.base.v3_0_0.fastapi import FastAPITestCase +from tests.asyncapi.base.v3_0_0.publisher import PublisherTestcase + + +class TestRouterArguments(FastAPITestCase, FastAPICompatible): + broker_factory = staticmethod(lambda: RedisRouter().broker) + router_factory = RedisRouter + broker_wrapper = staticmethod(TestRedisBroker) + + def build_app(self, router): + return router + + +class TestRouterPublisher(PublisherTestcase): + broker_factory = staticmethod(lambda: RedisRouter().broker) + + def build_app(self, router): + return router diff --git a/tests/asyncapi/redis/v3_0_0/test_naming.py b/tests/asyncapi/redis/v3_0_0/test_naming.py new file mode 100644 index 0000000000..098e7ed2e9 --- /dev/null +++ b/tests/asyncapi/redis/v3_0_0/test_naming.py @@ -0,0 +1,109 @@ +import pytest + +from faststream.redis import RedisBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.naming import NamingTestCase + + +class TestNaming(NamingTestCase): + broker_class = RedisBroker + + def test_base(self) -> None: + broker = self.broker_class() + + @broker.subscriber("test") + async def handle() -> None: ... + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": { + "test:Handle": { + "address": "test:Handle", + "bindings": { + "redis": { + "bindingVersion": "custom", + "channel": "test", + "method": "subscribe", + }, + }, + "servers": [{"$ref": "#/servers/development"}], + "messages": { + "SubscribeMessage": { + "$ref": "#/components/messages/test:Handle:SubscribeMessage", + }, + }, + }, + }, + "operations": { + "test:HandleSubscribe": { + "action": "receive", + "channel": { + "$ref": "#/channels/test:Handle", + }, + "messages": [ + {"$ref": "#/channels/test:Handle/messages/SubscribeMessage"}, + ], + }, + }, + "components": { + "messages": { + "test:Handle:SubscribeMessage": { + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/EmptyPayload"}, + "title": "test:Handle:SubscribeMessage", + }, + }, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "redis", + "protocolVersion": "custom", + "host": "localhost:6379", + "pathname": "", + }, + }, + }, schema + + @pytest.mark.parametrize( + "args", + ( + pytest.param({"channel": "test"}, id="channel"), + pytest.param({"list": "test"}, id="list"), + pytest.param({"stream": "test"}, id="stream"), + ), + ) + def test_subscribers_variations(self, args) -> None: + broker = self.broker_class() + + @broker.subscriber(**args) + async def handle() -> None: ... + + schema = AsyncAPI(broker) + assert list(schema.to_jsonable()["channels"].keys()) == ["test:Handle"] + + @pytest.mark.parametrize( + "args", + ( + pytest.param({"channel": "test"}, id="channel"), + pytest.param({"list": "test"}, id="list"), + pytest.param({"stream": "test"}, id="stream"), + ), + ) + def test_publisher_variations(self, args) -> None: + broker = self.broker_class() + + @broker.publisher(**args) + async def handle() -> None: ... + + schema = AsyncAPI(broker) + assert list(schema.to_jsonable()["channels"].keys()) == ["test:Publisher"] diff --git a/tests/asyncapi/redis/v3_0_0/test_publisher.py b/tests/asyncapi/redis/v3_0_0/test_publisher.py new file mode 100644 index 0000000000..ac25a1efa0 --- /dev/null +++ b/tests/asyncapi/redis/v3_0_0/test_publisher.py @@ -0,0 +1,50 @@ +from faststream.redis import RedisBroker +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v3_0_0.publisher import PublisherTestcase + + +class TestArguments(PublisherTestcase): + broker_factory = RedisBroker + + def test_channel_publisher(self) -> None: + broker = self.broker_factory() + + @broker.publisher("test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": { + "bindingVersion": "custom", + "channel": "test", + "method": "publish", + }, + } + + def test_list_publisher(self) -> None: + broker = self.broker_factory() + + @broker.publisher(list="test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": {"bindingVersion": "custom", "channel": "test", "method": "rpush"}, + } + + def test_stream_publisher(self) -> None: + broker = self.broker_factory() + + @broker.publisher(stream="test") + async def handle(msg) -> None: ... + + schema = AsyncAPI(self.build_app(broker), schema_version="3.0.0").to_jsonable() + key = tuple(schema["channels"].keys())[0] # noqa: RUF015 + + assert schema["channels"][key]["bindings"] == { + "redis": {"bindingVersion": "custom", "channel": "test", "method": "xadd"}, + } diff --git a/tests/asyncapi/redis/v3_0_0/test_router.py b/tests/asyncapi/redis/v3_0_0/test_router.py new file mode 100644 index 0000000000..14dc2c351e --- /dev/null +++ b/tests/asyncapi/redis/v3_0_0/test_router.py @@ -0,0 +1,104 @@ +from faststream.redis import RedisBroker, RedisPublisher, RedisRoute, RedisRouter +from faststream.specification.asyncapi import AsyncAPI +from tests.asyncapi.base.v2_6_0.arguments import ArgumentsTestcase +from tests.asyncapi.base.v2_6_0.publisher import PublisherTestcase +from tests.asyncapi.base.v3_0_0.router import RouterTestcase + + +class TestRouter(RouterTestcase): + broker_class = RedisBroker + router_class = RedisRouter + route_class = RedisRoute + publisher_class = RedisPublisher + + def test_prefix(self) -> None: + broker = self.broker_class() + + router = self.router_class(prefix="test_") + + @router.subscriber("test") + async def handle(msg) -> None: ... + + broker.include_router(router) + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "info": {"title": "FastStream", "version": "0.1.0", "description": ""}, + "asyncapi": "3.0.0", + "defaultContentType": "application/json", + "servers": { + "development": { + "host": "localhost:6379", + "pathname": "", + "protocol": "redis", + "protocolVersion": "custom", + }, + }, + "channels": { + "test_test:Handle": { + "address": "test_test:Handle", + "servers": [{"$ref": "#/servers/development"}], + "messages": { + "SubscribeMessage": { + "$ref": "#/components/messages/test_test:Handle:SubscribeMessage", + }, + }, + "bindings": { + "redis": { + "channel": "test_test", + "method": "subscribe", + "bindingVersion": "custom", + }, + }, + }, + }, + "operations": { + "test_test:HandleSubscribe": { + "action": "receive", + "messages": [ + { + "$ref": "#/channels/test_test:Handle/messages/SubscribeMessage", + }, + ], + "channel": {"$ref": "#/channels/test_test:Handle"}, + }, + }, + "components": { + "messages": { + "test_test:Handle:SubscribeMessage": { + "title": "test_test:Handle:SubscribeMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": { + "$ref": "#/components/schemas/Handle:Message:Payload", + }, + }, + }, + "schemas": { + "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + }, + }, + } + + +class TestRouterArguments(ArgumentsTestcase): + broker_class = RedisRouter + + def build_app(self, router): + broker = RedisBroker() + broker.include_router(router) + return broker + + +class TestRouterPublisher(PublisherTestcase): + broker_class = RedisRouter + + def build_app(self, router): + broker = RedisBroker() + broker.include_router(router) + return broker diff --git a/tests/asyncapi/redis/v3_0_0/test_security.py b/tests/asyncapi/redis/v3_0_0/test_security.py new file mode 100644 index 0000000000..c98547a7ff --- /dev/null +++ b/tests/asyncapi/redis/v3_0_0/test_security.py @@ -0,0 +1,128 @@ +import ssl + +from faststream.redis import RedisBroker +from faststream.security import ( + BaseSecurity, + SASLPlaintext, +) +from faststream.specification.asyncapi import AsyncAPI + + +def test_base_security_schema() -> None: + ssl_context = ssl.create_default_context() + security = BaseSecurity(ssl_context=ssl_context) + + broker = RedisBroker("rediss://localhost:6379/", security=security) + + assert ( + broker.specification.url + == ["rediss://localhost:6379/"] # pragma: allowlist secret + ) # pragma: allowlist secret + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": {"messages": {}, "schemas": {}, "securitySchemes": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "rediss", + "protocolVersion": "custom", + "security": [], + "host": "localhost:6379", + "pathname": "/", + }, + }, + } + + +def test_plaintext_security_schema() -> None: + ssl_context = ssl.create_default_context() + + security = SASLPlaintext( + ssl_context=ssl_context, + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = RedisBroker("redis://localhost:6379/", security=security) + + assert ( + broker.specification.url + == ["redis://localhost:6379/"] # pragma: allowlist secret + ) # pragma: allowlist secret + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": { + "messages": {}, + "schemas": {}, + "securitySchemes": {"user-password": {"type": "userPassword"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "redis", + "protocolVersion": "custom", + "security": [{"user-password": []}], + "host": "localhost:6379", + "pathname": "/", + }, + }, + } + + +def test_plaintext_security_schema_without_ssl() -> None: + security = SASLPlaintext( + username="admin", + password="password", # pragma: allowlist secret + ) + + broker = RedisBroker("redis://localhost:6379/", security=security) + + assert ( + broker.specification.url + == ["redis://localhost:6379/"] # pragma: allowlist secret + ) # pragma: allowlist secret + + schema = AsyncAPI( + broker, + schema_version="3.0.0", + ).to_jsonable() + + assert schema == { + "asyncapi": "3.0.0", + "channels": {}, + "operations": {}, + "components": { + "messages": {}, + "schemas": {}, + "securitySchemes": {"user-password": {"type": "userPassword"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "redis", + "protocolVersion": "custom", + "security": [{"user-password": []}], + "host": "localhost:6379", + "pathname": "/", + }, + }, + } diff --git a/tests/asyncapi/test_asgi.py b/tests/asyncapi/test_asgi.py index 509245a6ea..70c2772422 100644 --- a/tests/asyncapi/test_asgi.py +++ b/tests/asyncapi/test_asgi.py @@ -1,16 +1,21 @@ +import pytest + from faststream.asgi import AsgiFastStream, get, make_ping_asgi -from faststream.asyncapi.generate import get_app_schema from faststream.kafka import KafkaBroker +from faststream.specification.asyncapi.v2_6_0 import get_app_schema -def test_asgi(): +@pytest.mark.xfail( + reason="We still don't know how to pass asgi routes to AsyncAPI specification object" +) +def test_asgi() -> None: broker = KafkaBroker() @get - async def handler(): ... + async def handler() -> None: ... @get(include_in_schema=False) - async def handler2(): ... + async def handler2() -> None: ... app = AsgiFastStream( broker, diff --git a/tests/brokers/base/basic.py b/tests/brokers/base/basic.py index e550393052..cd8a4b65e0 100644 --- a/tests/brokers/base/basic.py +++ b/tests/brokers/base/basic.py @@ -1,13 +1,38 @@ -from typing import Any, Dict, Tuple +from abc import abstractmethod +from typing import Any + +from faststream._internal.broker import BrokerUsecase +from faststream._internal.broker.router import BrokerRouter class BaseTestcaseConfig: timeout: float = 3.0 + @abstractmethod + def get_broker( + self, + apply_types: bool = False, + **kwargs: Any, + ) -> BrokerUsecase[Any, Any]: + raise NotImplementedError + + def patch_broker( + self, + broker: BrokerUsecase, + **kwargs: Any, + ) -> BrokerUsecase: + return broker + def get_subscriber_params( - self, *args: Any, **kwargs: Any - ) -> Tuple[ - Tuple[Any, ...], - Dict[str, Any], + self, + *args: Any, + **kwargs: Any, + ) -> tuple[ + tuple[Any, ...], + dict[str, Any], ]: return args, kwargs + + @abstractmethod + def get_router(self, **kwargs: Any) -> BrokerRouter: + raise NotImplementedError diff --git a/tests/brokers/base/connection.py b/tests/brokers/base/connection.py index 1614b6151b..7203300bd1 100644 --- a/tests/brokers/base/connection.py +++ b/tests/brokers/base/connection.py @@ -1,57 +1,31 @@ -from typing import Type +from typing import Any import pytest -from faststream.broker.core.usecase import BrokerUsecase +from faststream._internal.broker import BrokerUsecase class BrokerConnectionTestcase: - broker: Type[BrokerUsecase] + broker: type[BrokerUsecase] - def get_broker_args(self, settings): + def get_broker_args(self, settings: Any) -> dict[str, Any]: return {} - @pytest.mark.asyncio + @pytest.mark.asyncio() async def ping(self, broker) -> bool: return await broker.ping(timeout=5.0) - @pytest.mark.asyncio - async def test_close_before_start(self, async_mock): + @pytest.mark.asyncio() + async def test_close_before_start(self) -> None: br = self.broker() assert br._connection is None await br.close() - br._connection = async_mock - await br._close() assert not br.running - @pytest.mark.asyncio - async def test_init_connect_by_url(self, settings): + @pytest.mark.asyncio() + async def test_connect(self, settings: Any) -> None: kwargs = self.get_broker_args(settings) broker = self.broker(**kwargs) await broker.connect() assert await self.ping(broker) await broker.close() - - @pytest.mark.asyncio - async def test_connection_by_url(self, settings): - kwargs = self.get_broker_args(settings) - broker = self.broker() - await broker.connect(**kwargs) - assert await self.ping(broker) - await broker.close() - - @pytest.mark.asyncio - async def test_connect_by_url_priority(self, settings): - kwargs = self.get_broker_args(settings) - broker = self.broker("wrong_url") - await broker.connect(**kwargs) - assert await self.ping(broker) - await broker.close() - - @pytest.mark.asyncio - async def test_ping_timeout(self, settings): - kwargs = self.get_broker_args(settings) - broker = self.broker("wrong_url") - await broker.connect(**kwargs) - assert not await broker.ping(timeout=1e-24) - await broker.close() diff --git a/tests/brokers/base/consume.py b/tests/brokers/base/consume.py index 0e7b07b698..e8e9dd156a 100644 --- a/tests/brokers/base/consume.py +++ b/tests/brokers/base/consume.py @@ -1,39 +1,29 @@ import asyncio -from abc import abstractmethod -from typing import Any -from unittest.mock import MagicMock +from unittest.mock import MagicMock, call import anyio import pytest from pydantic import BaseModel from faststream import Context, Depends -from faststream.broker.core.usecase import BrokerUsecase from faststream.exceptions import StopConsume from .basic import BaseTestcaseConfig -@pytest.mark.asyncio +@pytest.mark.asyncio() class BrokerConsumeTestcase(BaseTestcaseConfig): - @abstractmethod - def get_broker(self, broker: BrokerUsecase) -> BrokerUsecase[Any, Any]: - raise NotImplementedError - - def patch_broker(self, broker: BrokerUsecase[Any, Any]) -> BrokerUsecase[Any, Any]: - return broker - async def test_consume( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() consume_broker = self.get_broker() args, kwargs = self.get_subscriber_params(queue) @consume_broker.subscriber(*args, **kwargs) - def subscriber(m): + def subscriber(m) -> None: event.set() async with self.patch_broker(consume_broker) as br: @@ -52,7 +42,7 @@ async def test_consume_from_multi( self, queue: str, mock: MagicMock, - ): + ) -> None: consume_broker = self.get_broker() consume = asyncio.Event() @@ -63,7 +53,7 @@ async def test_consume_from_multi( @consume_broker.subscriber(*args, **kwargs) @consume_broker.subscriber(*args2, **kwargs2) - def subscriber(m): + def subscriber(m) -> None: mock() if not consume.is_set(): consume.set() @@ -82,15 +72,15 @@ def subscriber(m): timeout=self.timeout, ) - assert consume2.is_set() assert consume.is_set() + assert consume2.is_set() assert mock.call_count == 2 async def test_consume_double( self, queue: str, mock: MagicMock, - ): + ) -> None: consume_broker = self.get_broker() consume = asyncio.Event() @@ -99,7 +89,7 @@ async def test_consume_double( args, kwargs = self.get_subscriber_params(queue) @consume_broker.subscriber(*args, **kwargs) - async def handler(m): + async def handler(m) -> None: mock() if not consume.is_set(): consume.set() @@ -126,7 +116,7 @@ async def test_different_consume( self, queue: str, mock: MagicMock, - ): + ) -> None: consume_broker = self.get_broker() consume = asyncio.Event() @@ -135,7 +125,7 @@ async def test_different_consume( args, kwargs = self.get_subscriber_params(queue) @consume_broker.subscriber(*args, **kwargs) - def handler(m): + def handler(m) -> None: mock.handler() consume.set() @@ -143,7 +133,7 @@ def handler(m): args, kwargs = self.get_subscriber_params(another_topic) @consume_broker.subscriber(*args, **kwargs) - def handler2(m): + def handler2(m) -> None: mock.handler2() consume2.set() @@ -168,7 +158,7 @@ async def test_consume_with_filter( self, queue: str, mock: MagicMock, - ): + ) -> None: consume_broker = self.get_broker() consume = asyncio.Event() @@ -181,12 +171,12 @@ async def test_consume_with_filter( sub = consume_broker.subscriber(*args, **kwargs) @sub(filter=lambda m: m.content_type == "application/json") - async def handler(m): + async def handler(m) -> None: mock.handler(m) consume.set() @sub - async def handler2(m): + async def handler2(m) -> None: mock.handler2(m) consume2.set() @@ -210,13 +200,14 @@ async def handler2(m): async def test_consume_validate_false( self, queue: str, - event: asyncio.Event, mock: MagicMock, - ): - consume_broker = self.get_broker() + ) -> None: + event = asyncio.Event() - consume_broker._is_apply_types = True - consume_broker._is_validate = False + consume_broker = self.get_broker( + apply_types=True, + serializer=None, + ) class Foo(BaseModel): x: int @@ -227,7 +218,9 @@ def dependency() -> str: args, kwargs = self.get_subscriber_params(queue) @consume_broker.subscriber(*args, **kwargs) - async def handler(m: Foo, dep: int = Depends(dependency), broker=Context()): + async def handler( + m: Foo, dep: int = Depends(dependency), broker=Context() + ) -> None: mock(m, dep, broker) event.set() @@ -245,14 +238,12 @@ async def handler(m: Foo, dep: int = Depends(dependency), broker=Context()): assert event.is_set() mock.assert_called_once_with({"x": 1}, "100", consume_broker) - async def test_dynamic_sub( - self, - queue: str, - event: asyncio.Event, - ): + async def test_dynamic_sub(self, queue: str) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() - async def subscriber(m): + async def subscriber(m) -> None: event.set() async with self.patch_broker(consume_broker) as br: @@ -261,7 +252,6 @@ async def subscriber(m): args, kwargs = self.get_subscriber_params(queue) sub = br.subscriber(*args, **kwargs) sub(subscriber) - br.setup_subscriber(sub) await sub.start() await br.publish("hello", queue) @@ -273,13 +263,13 @@ async def subscriber(m): assert event.is_set() - async def test_get_one_conflicts_with_handler(self, queue): + async def test_get_one_conflicts_with_handler(self, queue) -> None: broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue) subscriber = broker.subscriber(*args, **kwargs) @subscriber - async def t(): ... + async def t() -> None: ... async with self.patch_broker(broker) as br: await br.start() @@ -288,14 +278,13 @@ async def t(): ... await subscriber.get_one(timeout=1e-24) -@pytest.mark.asyncio +@pytest.mark.asyncio() class BrokerRealConsumeTestcase(BrokerConsumeTestcase): async def test_get_one( self, queue: str, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue) @@ -304,10 +293,10 @@ async def test_get_one( async with self.patch_broker(broker) as br: await br.start() - async def consume(): + async def consume() -> None: mock(await subscriber.get_one(timeout=self.timeout)) - async def publish(): + async def publish() -> None: await anyio.sleep(1e-24) await br.publish("test_message", queue) @@ -328,7 +317,7 @@ async def test_get_one_timeout( self, queue: str, mock: MagicMock, - ): + ) -> None: broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue) subscriber = broker.subscriber(*args, **kwargs) @@ -339,13 +328,14 @@ async def test_get_one_timeout( mock(await subscriber.get_one(timeout=1e-24)) mock.assert_called_once_with(None) - @pytest.mark.slow + @pytest.mark.slow() async def test_stop_consume_exc( self, queue: str, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() args, kwargs = self.get_subscriber_params(queue) @@ -354,7 +344,7 @@ async def test_stop_consume_exc( def subscriber(m): mock() event.set() - raise StopConsume() + raise StopConsume async with self.patch_broker(consume_broker) as br: await br.start() @@ -371,3 +361,45 @@ def subscriber(m): assert event.is_set() mock.assert_called_once() + + @pytest.mark.asyncio() + async def test_iteration( + self, + queue: str, + mock: MagicMock, + ) -> None: + expected_messages = ("test_message_1", "test_message_2") + + broker = self.get_broker(apply_types=True) + + args, kwargs = self.get_subscriber_params(queue) + subscriber = broker.subscriber(*args, **kwargs) + + async with self.patch_broker(broker) as br: + await br.start() + + async def publish_test_message(): + for msg in expected_messages: + await br.publish(msg, queue) + + async def consume(): + index_message = 0 + async for msg in subscriber: + result_message = await msg.decode() + + mock(result_message) + + index_message += 1 + if index_message >= len(expected_messages): + break + + await asyncio.wait( + ( + asyncio.create_task(consume()), + asyncio.create_task(publish_test_message()), + ), + timeout=self.timeout, + ) + + calls = [call(msg) for msg in expected_messages] + mock.assert_has_calls(calls=calls) diff --git a/tests/brokers/base/fastapi.py b/tests/brokers/base/fastapi.py index 0e7208a7a0..9b87b119ac 100644 --- a/tests/brokers/base/fastapi.py +++ b/tests/brokers/base/fastapi.py @@ -1,36 +1,38 @@ import asyncio from contextlib import asynccontextmanager -from typing import Callable, Type +from typing import Annotated, Any, TypeVar from unittest.mock import Mock import pytest from fastapi import BackgroundTasks, Depends, FastAPI, Header from fastapi.exceptions import RequestValidationError from fastapi.testclient import TestClient -from typing_extensions import Annotated, Any, TypeVar - -from faststream import Context as FSContext -from faststream import Depends as FSDepends -from faststream import Response, context -from faststream.broker.core.usecase import BrokerUsecase -from faststream.broker.fastapi.context import Context -from faststream.broker.fastapi.route import StreamMessage -from faststream.broker.fastapi.router import StreamRouter -from faststream.broker.router import BrokerRouter + +from faststream import ( + Context as FSContext, + Depends as FSDepends, + Response, +) +from faststream._internal.broker import BrokerUsecase +from faststream._internal.broker.router import BrokerRouter +from faststream._internal.fastapi.context import Context +from faststream._internal.fastapi.route import StreamMessage +from faststream._internal.fastapi.router import StreamRouter from faststream.exceptions import SetupError -from faststream.types import AnyCallable from .basic import BaseTestcaseConfig Broker = TypeVar("Broker", bound=BrokerUsecase) -@pytest.mark.asyncio +@pytest.mark.asyncio() class FastAPITestcase(BaseTestcaseConfig): - router_class: Type[StreamRouter[BrokerUsecase]] - broker_router_class: Type[BrokerRouter[Any]] + router_class: type[StreamRouter[BrokerUsecase]] + broker_router_class: type[BrokerRouter[Any]] + + async def test_base_real(self, mock: Mock, queue: str) -> None: + event = asyncio.Event() - async def test_base_real(self, mock: Mock, queue: str, event: asyncio.Event): router = self.router_class() args, kwargs = self.get_subscriber_params(queue) @@ -53,17 +55,19 @@ async def hello(msg): assert event.is_set() mock.assert_called_with("hi") - async def test_background(self, mock: Mock, queue: str, event: asyncio.Event): + async def test_background( + self, mock: Mock, queue: str, event: asyncio.Event + ) -> None: router = self.router_class() - def task(msg): + def task(msg: Any) -> None: event.set() return mock(msg) args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def hello(msg, tasks: BackgroundTasks): + async def hello(msg, tasks: BackgroundTasks) -> None: tasks.add_task(task, msg) async with router.broker: @@ -76,26 +80,62 @@ async def hello(msg, tasks: BackgroundTasks): timeout=self.timeout, ) - assert event.is_set() mock.assert_called_with("hi") - async def test_context(self, mock: Mock, queue: str, event: asyncio.Event): + async def test_context(self, mock: Mock, queue: str, event: asyncio.Event) -> None: router = self.router_class() + context = router.context context_key = "message.headers" args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def hello(msg=Context(context_key)): - event.set() - return mock(msg == context.resolve(context_key)) + async def hello(msg: Any = Context(context_key)) -> None: + try: + mock(msg == context.resolve(context_key) and msg["1"] == "1") + finally: + event.set() async with router.broker: await router.broker.start() await asyncio.wait( ( - asyncio.create_task(router.broker.publish("", queue)), + asyncio.create_task( + router.broker.publish("", queue, headers={"1": "1"}) + ), + asyncio.create_task(event.wait()), + ), + timeout=self.timeout, + ) + + assert event.is_set() + mock.assert_called_with(True) + + async def test_context_annotated( + self, mock: Mock, queue: str, event: asyncio.Event + ) -> None: + router = self.router_class() + context = router.context + + context_key = "message.headers" + + args, kwargs = self.get_subscriber_params(queue) + + @router.subscriber(*args, **kwargs) + async def hello(msg: Annotated[Any, Context(context_key)]) -> None: + try: + mock(msg == context.resolve(context_key) and msg["1"] == "1") + finally: + event.set() + + async with router.broker: + await router.broker.start() + await asyncio.wait( + ( + asyncio.create_task( + router.broker.publish("", queue, headers={"1": "1"}) + ), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -104,13 +144,14 @@ async def hello(msg=Context(context_key)): assert event.is_set() mock.assert_called_with(True) - async def test_initial_context(self, queue: str, event: asyncio.Event): + async def test_initial_context(self, queue: str, event: asyncio.Event) -> None: router = self.router_class() + context = router.context args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def hello(msg: int, data=Context(queue, initial=set)): + async def hello(msg: int, data: set[int] = Context(queue, initial=set)) -> None: data.add(msg) if len(data) == 2: event.set() @@ -126,12 +167,13 @@ async def hello(msg: int, data=Context(queue, initial=set)): timeout=self.timeout, ) - assert event.is_set() assert context.get(queue) == {1, 2} context.reset_global(queue) - async def test_double_real(self, mock: Mock, queue: str, event: asyncio.Event): + async def test_double_real(self, mock: Mock, queue: str) -> None: + event = asyncio.Event() event2 = asyncio.Event() + router = self.router_class() args, kwargs = self.get_subscriber_params(queue) @@ -141,7 +183,7 @@ async def test_double_real(self, mock: Mock, queue: str, event: asyncio.Event): @sub1 @router.subscriber(*args2, **kwargs2) - async def hello(msg: str): + async def hello(msg: str) -> None: if event.is_set(): event2.set() else: @@ -168,21 +210,22 @@ async def test_base_publisher_real( self, mock: Mock, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) @router.publisher(queue + "resp") - async def m(): + async def m() -> str: return "hi" args2, kwargs2 = self.get_subscriber_params(queue + "resp") @router.subscriber(*args2, **kwargs2) - async def resp(msg): + async def resp(msg) -> None: event.set() mock(msg) @@ -228,13 +271,11 @@ async def subscriber(msg: StreamMessage) -> None: mock.assert_called_once_with(True) -@pytest.mark.asyncio +@pytest.mark.asyncio() class FastAPILocalTestcase(BaseTestcaseConfig): - router_class: Type[StreamRouter[BrokerUsecase]] - broker_test: Callable[[Broker], Broker] - build_message: AnyCallable + router_class: type[StreamRouter[BrokerUsecase]] - async def test_base(self, queue: str): + async def test_base(self, queue: str) -> None: router = self.router_class() app = FastAPI() @@ -243,22 +284,21 @@ async def test_base(self, queue: str): args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def hello(): + async def hello() -> str: return "hi" - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker) as br: with TestClient(app) as client: - assert client.app_state["broker"] is router.broker + assert client.app_state["broker"] is br - r = await router.broker.publish( + r = await br.request( "hi", queue, - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "hi", r + assert await r.decode() == "hi", r - async def test_request(self, queue: str): + async def test_request(self, queue: str) -> None: """Local test due request exists in all TestClients.""" router = self.router_class(setup_state=False) @@ -270,11 +310,11 @@ async def test_request(self, queue: str): async def hello(): return Response("Hi!", headers={"x-header": "test"}) - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker) as br: with TestClient(app) as client: assert not client.app_state.get("broker") - r = await router.broker.request( + r = await br.request( "hi", queue, timeout=0.5, @@ -282,7 +322,7 @@ async def hello(): assert await r.decode() == "Hi!" assert r.headers["x-header"] == "test" - async def test_base_without_state(self, queue: str): + async def test_base_without_state(self, queue: str) -> None: router = self.router_class(setup_state=False) app = FastAPI() @@ -290,22 +330,21 @@ async def test_base_without_state(self, queue: str): args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def hello(): + async def hello() -> str: return "hi" - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker) as br: with TestClient(app) as client: assert not client.app_state.get("broker") - r = await router.broker.publish( + r = await br.request( "hi", queue, - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "hi" + assert await r.decode() == "hi", r - async def test_invalid(self, queue: str): + async def test_invalid(self, queue: str) -> None: router = self.router_class() app = FastAPI() @@ -313,16 +352,16 @@ async def test_invalid(self, queue: str): args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def hello(msg: int): ... + async def hello(msg: int) -> None: ... app.include_router(router) - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker) as br: with TestClient(app): with pytest.raises(RequestValidationError): - await router.broker.publish("hi", queue) + await br.publish("hi", queue) - async def test_headers(self, queue: str): + async def test_headers(self, queue: str) -> None: router = self.router_class() args, kwargs = self.get_subscriber_params(queue) @@ -331,17 +370,16 @@ async def test_headers(self, queue: str): async def hello(w=Header()): return w - async with self.broker_test(router.broker): - r = await router.broker.publish( + async with self.patch_broker(router.broker) as br: + r = await br.request( "", queue, headers={"w": "hi"}, - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "hi" + assert await r.decode() == "hi", r - async def test_depends(self, mock: Mock, queue: str): + async def test_depends(self, mock: Mock, queue: str) -> None: router = self.router_class() def dep(a): @@ -354,18 +392,47 @@ def dep(a): async def hello(a, w=Depends(dep)): return w - async with self.broker_test(router.broker): - r = await router.broker.publish( + async with self.patch_broker(router.broker) as br: + r = await br.request( + {"a": "hi"}, + queue, + timeout=0.5, + ) + assert await r.decode() == "hi", r + + mock.assert_called_once_with("hi") + + async def test_mixed_depends(self, mock: Mock, queue: str) -> None: + router = self.router_class() + + def dep(a: str) -> str: + mock(a) + return a + + args, kwargs = self.get_subscriber_params(queue) + + @router.subscriber(*args, **kwargs) + async def hello( + a: str, + w: Annotated[ + str, + Depends(dep), + FSDepends(dep), # will be ignored + ], + ) -> str: + return w + + async with self.patch_broker(router.broker) as br: + r = await br.request( {"a": "hi"}, queue, - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "hi" + assert await r.decode() == "hi", r mock.assert_called_once_with("hi") - async def test_depends_from_fastdepends_default(self, queue: str): + async def test_depends_from_fastdepends_default(self, queue: str) -> None: router = self.router_class() args, kwargs = self.get_subscriber_params(queue) @@ -379,11 +446,11 @@ def sub(d: Any = FSDepends(lambda: 1)) -> None: ... app.include_router(router) with pytest.raises(SetupError): # noqa: PT012 - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker): with TestClient(app): ... - async def test_depends_from_fastdepends_annotated(self, queue: str): + async def test_depends_from_fastdepends_annotated(self, queue: str) -> None: router = self.router_class() args, kwargs = self.get_subscriber_params(queue) @@ -397,11 +464,11 @@ def sub(d: Annotated[Any, FSDepends(lambda: 1)]) -> None: ... app.include_router(router) with pytest.raises(SetupError): # noqa: PT012 - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker): with TestClient(app): ... - async def test_depends_combined_annotated(self, queue: str): + async def test_depends_combined_annotated(self, queue: str) -> None: router = self.router_class() args, kwargs = self.get_subscriber_params(queue) @@ -416,106 +483,84 @@ def sub( app = FastAPI() app.include_router(router) - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker): with TestClient(app): ... - async def test_context_annotated(self, queue: str, event: asyncio.Event): + async def test_faststream_context(self, queue: str) -> None: router = self.router_class() - context_key = "message.headers" - - args, kwargs = self.get_subscriber_params(queue) - - @router.subscriber(*args, **kwargs) - async def hello(msg: Annotated[Any, Context(context_key)]): ... - - app = FastAPI() - app.include_router(router) - - async with self.broker_test(router.broker): - with TestClient(app): - ... - - async def test_faststream_context(self, queue: str, event: asyncio.Event): - router = self.router_class() - - context_key = "message.headers" - args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def hello(msg=FSContext(context_key)): ... + async def hello(msg: Any = FSContext()) -> None: ... app = FastAPI() app.include_router(router) with pytest.raises(SetupError): # noqa: PT012 - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker): with TestClient(app): ... - async def test_faststream_context_annotated(self, queue: str, event: asyncio.Event): + async def test_faststream_context_annotated(self, queue: str) -> None: router = self.router_class() - context_key = "message.headers" - args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def hello(msg: Annotated[Any, FSContext(context_key)]): ... + async def hello(msg: Annotated[Any, FSContext()]) -> None: ... app = FastAPI() app.include_router(router) with pytest.raises(SetupError): # noqa: PT012 - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker): with TestClient(app): ... - async def test_combined_context_annotated(self, queue: str, event: asyncio.Event): + async def test_combined_context_annotated(self, queue: str) -> None: router = self.router_class() - context_key = "message.headers" - args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) async def hello( - msg: Annotated[Any, Context(context_key), FSContext(context_key)], - ): ... + msg: Annotated[ + Any, + Context("message.headers"), + FSContext("message.headers"), + ], + ) -> None: ... app = FastAPI() app.include_router(router) - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker): with TestClient(app): ... - async def test_nested_combined_context_annotated( - self, queue: str, event: asyncio.Event - ): + async def test_nested_combined_context_annotated(self, queue: str) -> None: router = self.router_class() - context_key = "message.headers" - args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) async def hello( msg: Annotated[ - Annotated[Any, FSContext(context_key)], Context(context_key) + Annotated[Any, FSContext("message.headers")], + Context("message.headers"), ], - ): ... + ) -> None: ... app = FastAPI() app.include_router(router) - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker): with TestClient(app): ... - async def test_yield_depends(self, mock: Mock, queue: str): + async def test_yield_depends(self, mock: Mock, queue: str) -> None: router = self.router_class() def dep(a): @@ -531,20 +576,19 @@ async def hello(a, w=Depends(dep)): assert not mock.close.call_count return w - async with self.broker_test(router.broker): - r = await router.broker.publish( + async with self.patch_broker(router.broker) as br: + r = await br.request( {"a": "hi"}, queue, - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "hi" + assert await r.decode() == "hi", r mock.start.assert_called_once() mock.close.assert_called_once() - async def test_router_depends(self, mock: Mock, queue: str): - def mock_dep(): + async def test_router_depends(self, mock: Mock, queue: str) -> None: + def mock_dep() -> None: mock() router = self.router_class(dependencies=(Depends(mock_dep, use_cache=False),)) @@ -555,14 +599,14 @@ def mock_dep(): async def hello(a): return a - async with self.broker_test(router.broker): - r = await router.broker.publish("hi", queue, rpc=True, rpc_timeout=0.5) - assert r == "hi" + async with self.patch_broker(router.broker) as br: + r = await br.request("hi", queue, timeout=0.5) + assert await r.decode() == "hi", r mock.assert_called_once() - async def test_subscriber_depends(self, mock: Mock, queue: str): - def mock_dep(): + async def test_subscriber_depends(self, mock: Mock, queue: str) -> None: + def mock_dep() -> None: mock() router = self.router_class() @@ -576,40 +620,39 @@ def mock_dep(): async def hello(a): return a - async with self.broker_test(router.broker): - r = await router.broker.publish( + async with self.patch_broker(router.broker) as br: + r = await br.request( "hi", queue, - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "hi" + assert await r.decode() == "hi", r mock.assert_called_once() - async def test_hooks(self, mock: Mock): + async def test_hooks(self, mock: Mock) -> None: router = self.router_class() app = FastAPI() app.include_router(router) @router.after_startup - def test_sync(app): + def test_sync(app) -> None: mock.sync_called() @router.after_startup - async def test_async(app): + async def test_async(app) -> None: mock.async_called() @router.on_broker_shutdown - def test_shutdown_sync(app): + def test_shutdown_sync(app) -> None: mock.sync_shutdown_called() @router.on_broker_shutdown - async def test_shutdown_async(app): + async def test_shutdown_async(app) -> None: mock.async_shutdown_called() - async with self.broker_test(router.broker), router.lifespan_context(app): + async with self.patch_broker(router.broker), router.lifespan_context(app): pass mock.sync_called.assert_called_once() @@ -617,7 +660,7 @@ async def test_shutdown_async(app): mock.sync_shutdown_called.assert_called_once() mock.async_shutdown_called.assert_called_once() - async def test_existed_lifespan_startup(self, mock: Mock): + async def test_existed_lifespan_startup(self, mock: Mock) -> None: @asynccontextmanager async def lifespan(app): mock.start() @@ -629,28 +672,31 @@ async def lifespan(app): app = FastAPI() app.include_router(router) - async with self.broker_test(router.broker), router.lifespan_context( - app - ) as context: + async with ( + self.patch_broker(router.broker), + router.lifespan_context( + app, + ) as context, + ): assert context["lifespan"] mock.start.assert_called_once() mock.close.assert_called_once() - async def test_subscriber_mock(self, queue: str): + async def test_subscriber_mock(self, queue: str) -> None: router = self.router_class() args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def m(): + async def m() -> str: return "hi" - async with self.broker_test(router.broker) as rb: + async with self.patch_broker(router.broker) as rb: await rb.publish("hello", queue) m.mock.assert_called_once_with("hello") - async def test_publisher_mock(self, queue: str): + async def test_publisher_mock(self, queue: str) -> None: router = self.router_class() publisher = router.publisher(queue + "resp") @@ -660,14 +706,14 @@ async def test_publisher_mock(self, queue: str): @publisher @sub - async def m(): + async def m() -> str: return "response" - async with self.broker_test(router.broker) as rb: + async with self.patch_broker(router.broker) as rb: await rb.publish("hello", queue) publisher.mock.assert_called_with("response") - async def test_include(self, queue: str): + async def test_include(self, queue: str) -> None: router = self.router_class() router2 = self.broker_router_class() @@ -676,67 +722,91 @@ async def test_include(self, queue: str): args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def hello(): + async def hello() -> str: return "hi" args2, kwargs2 = self.get_subscriber_params(queue + "1") @router2.subscriber(*args2, **kwargs2) - async def hello_router2(): + async def hello_router2() -> str: return "hi" router.include_router(router2) app.include_router(router) - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker) as br: with TestClient(app) as client: - assert client.app_state["broker"] is router.broker + assert client.app_state["broker"] is br - r = await router.broker.publish( + r = await br.request( "hi", queue, - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "hi" + assert await r.decode() == "hi", r - r = await router.broker.publish( + r = await br.request( "hi", queue + "1", - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "hi" + assert await r.decode() == "hi", r - async def test_dependency_overrides(self, mock: Mock, queue: str): + async def test_dependency_overrides(self, mock: Mock, queue: str) -> None: router = self.router_class() - router2 = self.router_class() - def dep1(): + def dep1() -> None: raise AssertionError + def dep2() -> None: + mock() + app = FastAPI() - app.dependency_overrides[dep1] = lambda: mock() + app.dependency_overrides[dep1] = dep2 args, kwargs = self.get_subscriber_params(queue) - @router2.subscriber(*args, **kwargs) - async def hello_router2(dep=Depends(dep1)): + @router.subscriber(*args, **kwargs) + async def hello_router2(dep: None = Depends(dep1)) -> str: return "hi" - router.include_router(router2) app.include_router(router) - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker) as br: with TestClient(app) as client: - assert client.app_state["broker"] is router.broker + assert client.app_state["broker"] is br - r = await router.broker.publish( + r = await br.request( "hi", queue, - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "hi" + assert await r.decode() == "hi", r mock.assert_called_once() + + async def test_nested_router(self, queue: str) -> None: + router = self.router_class() + router2 = self.router_class() + + app = FastAPI() + + args, kwargs = self.get_subscriber_params(queue) + + @router2.subscriber(*args, **kwargs) + async def hello_router2() -> str: + return "hi" + + router.include_router(router2) + app.include_router(router) + + async with self.patch_broker(router.broker) as br: + with TestClient(app) as client: + assert client.app_state["broker"] is br + + r = await br.request( + "hi", + queue, + timeout=0.5, + ) + assert await r.decode() == "hi", r diff --git a/tests/brokers/base/include_router.py b/tests/brokers/base/include_router.py new file mode 100644 index 0000000000..6156d24c17 --- /dev/null +++ b/tests/brokers/base/include_router.py @@ -0,0 +1,148 @@ +from typing import Any + +import pytest + +from faststream._internal.broker import BrokerRouter, BrokerUsecase + +from .basic import BaseTestcaseConfig + + +class IncludeTestcase(BaseTestcaseConfig): + def get_object(self, router: BrokerRouter[Any] | BrokerUsecase[Any, Any]) -> Any: + raise NotImplementedError + + def test_broker_middlewares(self) -> None: + broker = self.get_broker(middlewares=(1,)) + + obj = self.get_object(broker) + + broker_middlewars = obj._outer_config.broker_middlewares + assert tuple(broker_middlewars) == (1,), broker_middlewars + + def test_router_middlewares(self) -> None: + broker = self.get_broker(middlewares=(1,)) + + router = self.get_router(middlewares=[2]) + + obj = self.get_object(router) + + broker.include_router(router) + + broker_middlewars = obj._outer_config.broker_middlewares + assert tuple(broker_middlewars) == (1, 2), broker_middlewars + + def test_nested_router_middleware(self) -> None: + broker = self.get_broker(middlewares=(1,)) + + router = self.get_router(middlewares=[2]) + + router2 = self.get_router(middlewares=[3]) + + obj = self.get_object(router2) + + router.include_router(router2) + broker.include_router(router) + + broker_middlewars = obj._outer_config.broker_middlewares + assert tuple(broker_middlewars) == (1, 2, 3), broker_middlewars + + def test_include_router_with_middlewares(self) -> None: + broker = self.get_broker(middlewares=(1,)) + + router = self.get_router(middlewares=[3]) + + obj = self.get_object(router) + + broker.include_router(router, middlewares=[2]) + + broker_middlewars = obj._outer_config.broker_middlewares + assert tuple(broker_middlewars) == (1, 2, 3), broker_middlewars + + @pytest.mark.parametrize( + ("include_router", "include", "result"), + ( + pytest.param(False, True, False, id="visible router"), + pytest.param(True, True, True, id="invisible include"), + pytest.param(None, True, True, id="default router"), + pytest.param(False, False, False, id="ignore visible router"), + pytest.param( + True, + False, + False, + id="ignore invisible router", + ), + pytest.param(None, False, False, id="ignore default router"), + ), + ) + def test_router_include_in_schema( + self, include_router: bool | None, include: bool, result: bool + ) -> None: + broker = self.get_broker() + router = self.get_router(include_in_schema=include_router) + + obj = self.get_object(router) + broker.include_router(router, include_in_schema=include) + + assert obj.specification.include_in_schema is result + + +class IncludeSubscriberTestcase(IncludeTestcase): + def get_object(self, router: BrokerRouter[Any] | BrokerUsecase[Any, Any]) -> Any: + return router.subscriber("test") + + def test_graceful_timeout(self) -> None: + broker = self.get_broker(graceful_timeout=10) + router = self.get_router() + router2 = self.get_router() + + obj = self.get_object(router2) + + router.include_router(router2) + broker.include_router(router) + + assert obj._outer_config.graceful_timeout == 10 + + def test_simple_router_prefix(self) -> None: + broker = self.get_broker() + + router = self.get_router(prefix="1.") + obj = self.get_object(router) + + broker.include_router(router) + + assert obj._outer_config.prefix == "1." + + def test_nested_router_prefix(self) -> None: + broker = self.get_broker() + + router = self.get_router(prefix="1.") + + router2 = self.get_router(prefix="2.") + obj = self.get_object(router2) + + router.include_router(router2) + broker.include_router(router) + + assert obj._outer_config.prefix == "1.2." + + def test_complex_router_prefix(self) -> None: + broker = self.get_broker() + router = self.get_router(prefix="1.") + + router2 = self.get_router() + sub2 = self.get_object(router2) + + router3 = self.get_router(prefix="5.") + sub3 = self.get_object(router3) + + router2.include_router(router3, prefix="4.") + router.include_router(router2) + broker.include_router(router) + + assert sub2._outer_config.prefix == "1." + assert sub3._outer_config.prefix == "1.4.5." + + +class IncludePublisherTestcase(IncludeTestcase): + def get_object(self, router: BrokerRouter[Any] | BrokerUsecase[Any, Any]) -> Any: + return router.publisher("test") diff --git a/tests/brokers/base/middlewares.py b/tests/brokers/base/middlewares.py index 94a8cf5afb..598ff28242 100644 --- a/tests/brokers/base/middlewares.py +++ b/tests/brokers/base/middlewares.py @@ -1,87 +1,69 @@ import asyncio -from typing import Type -from unittest.mock import Mock, call +from unittest.mock import MagicMock, call import pytest from faststream import Context -from faststream.broker.core.usecase import BrokerUsecase -from faststream.broker.middlewares import BaseMiddleware, ExceptionMiddleware +from faststream._internal.basic_types import DecodedMessage from faststream.exceptions import SkipMessage -from faststream.types import DecodedMessage +from faststream.middlewares import BaseMiddleware, ExceptionMiddleware +from faststream.response import PublishCommand from .basic import BaseTestcaseConfig -@pytest.mark.asyncio +@pytest.mark.asyncio() class MiddlewaresOrderTestcase(BaseTestcaseConfig): - broker_class: Type[BrokerUsecase] - - def patch_broker(self, broker: BrokerUsecase) -> BrokerUsecase: - return broker - - async def test_broker_middleware_order( - self, event: asyncio.Event, queue: str, mock: Mock - ): + async def test_broker_middleware_order(self, queue: str, mock: MagicMock) -> None: class InnerMiddleware(BaseMiddleware): - async def __aenter__(self): + async def __aenter__(self) -> None: mock.enter_inner() mock.enter("inner") - async def __aexit__(self, *args): + async def __aexit__(self, *args) -> None: mock.exit_inner() mock.exit("inner") - async def consume_scope(self, call_next, msg): + async def consume_scope(self, call_next, msg) -> None: mock.consume_inner() mock.sub("inner") return await call_next(msg) - async def publish_scope(self, call_next, msg, *args, **kwargs): + async def publish_scope(self, call_next, cmd) -> None: mock.publish_inner() mock.pub("inner") - return await call_next(msg, *args, **kwargs) + return await call_next(cmd) class OuterMiddleware(BaseMiddleware): - async def __aenter__(self): + async def __aenter__(self) -> None: mock.enter_outer() mock.enter("outer") - async def __aexit__(self, *args): + async def __aexit__(self, *args) -> None: mock.exit_outer() mock.exit("outer") - async def consume_scope(self, call_next, msg): + async def consume_scope(self, call_next, msg) -> None: mock.consume_outer() mock.sub("outer") return await call_next(msg) - async def publish_scope(self, call_next, msg, *args, **kwargs): + async def publish_scope(self, call_next, cmd) -> None: mock.publish_outer() mock.pub("outer") - return await call_next(msg, *args, **kwargs) + return await call_next(cmd) - broker = self.broker_class( - middlewares=[OuterMiddleware, InnerMiddleware], - ) + broker = self.get_broker(middlewares=[OuterMiddleware, InnerMiddleware]) args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) async def handler(msg): - event.set() + pass async with self.patch_broker(broker) as br: - await br.start() - await asyncio.wait( - ( - asyncio.create_task(broker.publish("start", queue)), - asyncio.create_task(event.wait()), - ), - timeout=self.timeout, - ) + await br.publish(None, queue) - assert event.is_set() mock.consume_inner.assert_called_once() mock.consume_outer.assert_called_once() mock.publish_inner.assert_called_once() @@ -91,52 +73,38 @@ async def handler(msg): mock.exit_inner.assert_called_once() mock.exit_outer.assert_called_once() - assert [c.args[0] for c in mock.sub.call_args_list] == [ - "outer", - "inner", - ], mock.sub.call_args_list - assert [c.args[0] for c in mock.pub.call_args_list] == [ - "outer", - "inner", - ], mock.pub.call_args_list - assert [c.args[0] for c in mock.enter.call_args_list] == [ - "outer", - "inner", - ], mock.enter.call_args_list - assert [c.args[0] for c in mock.exit.call_args_list] == [ - "inner", - "outer", - ], mock.exit.call_args_list + assert [c.args[0] for c in mock.sub.call_args_list] == ["outer", "inner"] + assert [c.args[0] for c in mock.pub.call_args_list] == ["outer", "inner"] + assert [c.args[0] for c in mock.enter.call_args_list] == ["outer", "inner"] + assert [c.args[0] for c in mock.exit.call_args_list] == ["inner", "outer"] async def test_publisher_middleware_order( - self, event: asyncio.Event, queue: str, mock: Mock - ): + self, queue: str, mock: MagicMock + ) -> None: class InnerMiddleware(BaseMiddleware): - async def publish_scope(self, call_next, msg, *args, **kwargs): + async def publish_scope(self, call_next, cmd): mock.publish_inner() mock("inner") - return await call_next(msg, *args, **kwargs) + return await call_next(cmd) class MiddleMiddleware(BaseMiddleware): - async def publish_scope(self, call_next, msg, *args, **kwargs): + async def publish_scope(self, call_next, cmd): mock.publish_middle() mock("middle") - return await call_next(msg, *args, **kwargs) + return await call_next(cmd) class OuterMiddleware(BaseMiddleware): - async def publish_scope(self, call_next, msg, *args, **kwargs): + async def publish_scope(self, call_next, cmd): mock.publish_outer() mock("outer") - return await call_next(msg, *args, **kwargs) + return await call_next(cmd) - broker = self.broker_class( - middlewares=[OuterMiddleware], - ) + broker = self.get_broker(middlewares=[OuterMiddleware]) publisher = broker.publisher( queue, middlewares=[ - MiddleMiddleware(None).publish_scope, - InnerMiddleware(None).publish_scope, + MiddleMiddleware(None, context=None).publish_scope, + InnerMiddleware(None, context=None).publish_scope, ], ) @@ -144,50 +112,41 @@ async def publish_scope(self, call_next, msg, *args, **kwargs): @broker.subscriber(*args, **kwargs) async def handler(msg): - event.set() + pass - async with self.patch_broker(broker) as br: - await br.start() + async with self.patch_broker(broker): await publisher.publish(None, queue) mock.publish_inner.assert_called_once() mock.publish_middle.assert_called_once() mock.publish_outer.assert_called_once() - assert [c.args[0] for c in mock.call_args_list] == [ - "outer", - "middle", - "inner", - ], mock.call_args_list + + assert [c.args[0] for c in mock.call_args_list] == ["outer", "middle", "inner"] async def test_publisher_with_router_middleware_order( - self, - event: asyncio.Event, - queue: str, - mock: Mock, - ): + self, queue: str, mock: MagicMock + ) -> None: class InnerMiddleware(BaseMiddleware): - async def publish_scope(self, call_next, msg, *args, **kwargs): + async def publish_scope(self, call_next, cmd): mock.publish_inner() mock("inner") - return await call_next(msg, *args, **kwargs) + return await call_next(cmd) class MiddleMiddleware(BaseMiddleware): - async def publish_scope(self, call_next, msg, *args, **kwargs): + async def publish_scope(self, call_next, cmd): mock.publish_middle() mock("middle") - return await call_next(msg, *args, **kwargs) + return await call_next(cmd) class OuterMiddleware(BaseMiddleware): - async def publish_scope(self, call_next, msg, *args, **kwargs): + async def publish_scope(self, call_next, cmd): mock.publish_outer() mock("outer") - return await call_next(msg, *args, **kwargs) + return await call_next(cmd) - broker = self.broker_class( - middlewares=[OuterMiddleware], - ) - router = self.broker_class(middlewares=[MiddleMiddleware]) - router2 = self.broker_class(middlewares=[InnerMiddleware]) + broker = self.get_broker(middlewares=[OuterMiddleware]) + router = self.get_router(middlewares=[MiddleMiddleware]) + router2 = self.get_router(middlewares=[InnerMiddleware]) publisher = router2.publisher(queue) @@ -195,157 +154,108 @@ async def publish_scope(self, call_next, msg, *args, **kwargs): @router2.subscriber(*args, **kwargs) async def handler(msg): - event.set() + pass router.include_router(router2) broker.include_router(router) - async with self.patch_broker(broker) as br: - await br.start() + async with self.patch_broker(broker): await publisher.publish(None, queue) mock.publish_inner.assert_called_once() mock.publish_middle.assert_called_once() mock.publish_outer.assert_called_once() - assert [c.args[0] for c in mock.call_args_list] == [ - "outer", - "middle", - "inner", - ], mock.call_args_list + assert [c.args[0] for c in mock.call_args_list] == ["outer", "middle", "inner"] - async def test_consume_middleware_order( - self, event: asyncio.Event, queue: str, mock: Mock - ): + async def test_consume_middleware_order(self, queue: str, mock: MagicMock) -> None: class InnerMiddleware(BaseMiddleware): - async def consume_scope(self, call_next, msg): + async def consume_scope(self, call_next, cmd): mock.consume_inner() mock("inner") - return await call_next(msg) + return await call_next(cmd) class MiddleMiddleware(BaseMiddleware): - async def consume_scope(self, call_next, msg): + async def consume_scope(self, call_next, cmd): mock.consume_middle() mock("middle") - return await call_next(msg) + return await call_next(cmd) class OuterMiddleware(BaseMiddleware): - async def consume_scope(self, call_next, msg): + async def consume_scope(self, call_next, cmd): mock.consume_outer() mock("outer") - return await call_next(msg) + return await call_next(cmd) - broker = self.broker_class(middlewares=[OuterMiddleware]) + broker = self.get_broker(middlewares=[OuterMiddleware]) args, kwargs = self.get_subscriber_params( queue, middlewares=[ - MiddleMiddleware(None).consume_scope, - InnerMiddleware(None).consume_scope, + MiddleMiddleware(None, context=None).consume_scope, + InnerMiddleware(None, context=None).consume_scope, ], ) @broker.subscriber(*args, **kwargs) async def handler(msg): - event.set() + pass async with self.patch_broker(broker) as br: - await br.start() - await asyncio.wait( - ( - asyncio.create_task(broker.publish("start", queue)), - asyncio.create_task(event.wait()), - ), - timeout=self.timeout, - ) + await br.publish(None, queue) - assert event.is_set() mock.consume_inner.assert_called_once() mock.consume_middle.assert_called_once() mock.consume_outer.assert_called_once() - assert [c.args[0] for c in mock.call_args_list] == [ - "outer", - "middle", - "inner", - ], mock.call_args_list + assert [c.args[0] for c in mock.call_args_list] == ["outer", "middle", "inner"] async def test_consume_with_middleware_order( - self, event: asyncio.Event, queue: str, mock: Mock - ): + self, queue: str, mock: MagicMock + ) -> None: class InnerMiddleware(BaseMiddleware): async def consume_scope(self, call_next, cmd): - mock.consume_inner() mock("inner") return await call_next(cmd) class MiddleMiddleware(BaseMiddleware): async def consume_scope(self, call_next, cmd): - mock.consume_middle() mock("middle") return await call_next(cmd) class OuterMiddleware(BaseMiddleware): async def consume_scope(self, call_next, cmd): - mock.consume_outer() mock("outer") return await call_next(cmd) - broker = self.broker_class(middlewares=[OuterMiddleware]) - router = self.broker_class(middlewares=[MiddleMiddleware]) - router2 = self.broker_class(middlewares=[InnerMiddleware]) + broker = self.get_broker(middlewares=[OuterMiddleware]) + router = self.get_router(middlewares=[MiddleMiddleware]) + router2 = self.get_router(middlewares=[InnerMiddleware]) args, kwargs = self.get_subscriber_params(queue) @router2.subscriber(*args, **kwargs) async def handler(msg): - event.set() + pass router.include_router(router2) broker.include_router(router) - async with self.patch_broker(broker) as br: - await br.start() - await asyncio.wait( - ( - asyncio.create_task(broker.publish("start", queue)), - asyncio.create_task(event.wait()), - ), - timeout=self.timeout, - ) - - mock.consume_inner.assert_called_once() - mock.consume_middle.assert_called_once() - mock.consume_outer.assert_called_once() + await br.publish(None, queue) - assert event.is_set() - assert [c.args[0] for c in mock.call_args_list] == [ - "outer", - "middle", - "inner", - ], mock.call_args_list + call_order = [c.args[0] for c in mock.call_args_list] + assert call_order == ["outer", "middle", "inner"], call_order -@pytest.mark.asyncio +@pytest.mark.asyncio() class LocalMiddlewareTestcase(BaseTestcaseConfig): - broker_class: Type[BrokerUsecase] - - @pytest.fixture - def raw_broker(self): - return None - - def patch_broker( - self, raw_broker: BrokerUsecase, broker: BrokerUsecase - ) -> BrokerUsecase: - return broker - async def test_subscriber_middleware( self, - event: asyncio.Event, queue: str, - mock: Mock, - raw_broker, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + async def mid(call_next, msg): mock.start(await msg.decode()) result = await call_next(msg) @@ -353,22 +263,20 @@ async def mid(call_next, msg): event.set() return result - broker = self.broker_class() + broker = self.get_broker() args, kwargs = self.get_subscriber_params(queue, middlewares=(mid,)) @broker.subscriber(*args, **kwargs) - async def handler(m): + async def handler(m) -> str: mock.inner(m) return "end" - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("start", queue)), + asyncio.create_task(br.publish("start", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -381,12 +289,8 @@ async def handler(m): mock.end.assert_called_once() async def test_publisher_middleware( - self, - event: asyncio.Event, - queue: str, - mock: Mock, - raw_broker, - ): + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: async def mid(call_next, msg, **kwargs): mock.enter() result = await call_next(msg, **kwargs) @@ -395,24 +299,22 @@ async def mid(call_next, msg, **kwargs): event.set() return result - broker = self.broker_class() + broker = self.get_broker() args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) @broker.publisher(queue + "1", middlewares=(mid,)) @broker.publisher(queue + "2", middlewares=(mid,)) - async def handler(m): + async def handler(m) -> str: mock.inner(m) return "end" - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("start", queue)), + asyncio.create_task(br.publish("start", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -424,8 +326,10 @@ async def handler(m): assert mock.end.call_count == 2 async def test_local_middleware_not_shared_between_subscribers( - self, queue: str, mock: Mock, raw_broker - ): + self, + queue: str, + mock: MagicMock, + ) -> None: event1 = asyncio.Event() event2 = asyncio.Event() @@ -435,7 +339,7 @@ async def mid(call_next, msg): mock.end() return result - broker = self.broker_class() + broker = self.get_broker() args, kwargs = self.get_subscriber_params(queue) args2, kwargs2 = self.get_subscriber_params( @@ -445,7 +349,7 @@ async def mid(call_next, msg): @broker.subscriber(*args, **kwargs) @broker.subscriber(*args2, **kwargs2) - async def handler(m): + async def handler(m) -> str: if event1.is_set(): event2.set() else: @@ -453,10 +357,8 @@ async def handler(m): mock() return "" - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( asyncio.create_task(broker.publish("", queue)), @@ -474,8 +376,10 @@ async def handler(m): assert mock.call_count == 2 async def test_local_middleware_consume_not_shared_between_filters( - self, queue: str, mock: Mock, raw_broker - ): + self, + queue: str, + mock: MagicMock, + ) -> None: event1 = asyncio.Event() event2 = asyncio.Event() @@ -485,7 +389,7 @@ async def mid(call_next, msg): mock.end() return result - broker = self.broker_class() + broker = self.get_broker() args, kwargs = self.get_subscriber_params( queue, @@ -494,25 +398,23 @@ async def mid(call_next, msg): sub = broker.subscriber(*args, **kwargs) @sub(filter=lambda m: m.content_type == "application/json") - async def handler(m): + async def handler(m) -> str: event2.set() mock() return "" @sub(middlewares=(mid,)) - async def handler2(m): + async def handler2(m) -> str: event1.set() mock() return "" - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish({"msg": "hi"}, queue)), - asyncio.create_task(broker.publish("", queue)), + asyncio.create_task(br.publish({"msg": "hi"}, queue)), + asyncio.create_task(br.publish("", queue)), asyncio.create_task(event1.wait()), asyncio.create_task(event2.wait()), ), @@ -525,33 +427,33 @@ async def handler2(m): mock.end.assert_called_once() assert mock.call_count == 2 - async def test_error_traceback(self, queue: str, mock: Mock, event, raw_broker): + async def test_error_traceback( + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: async def mid(call_next, msg): try: result = await call_next(msg) except Exception as e: mock(isinstance(e, ValueError)) - raise e + raise else: return result - broker = self.broker_class() + broker = self.get_broker() args, kwargs = self.get_subscriber_params(queue, middlewares=(mid,)) @broker.subscriber(*args, **kwargs) async def handler2(m): event.set() - raise ValueError() - - broker = self.patch_broker(raw_broker, broker) + raise ValueError - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("", queue)), + asyncio.create_task(br.publish("", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -561,11 +463,11 @@ async def handler2(m): mock.assert_called_once_with(True) -@pytest.mark.asyncio +@pytest.mark.asyncio() class MiddlewareTestcase(LocalMiddlewareTestcase): async def test_global_middleware( - self, event: asyncio.Event, queue: str, mock: Mock, raw_broker - ): + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: class mid(BaseMiddleware): # noqa: N801 async def on_receive(self): mock.start(self.msg) @@ -575,22 +477,22 @@ async def after_processed(self, exc_type, exc_val, exc_tb): mock.end() return await super().after_processed(exc_type, exc_val, exc_tb) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker( + middlewares=(mid,), + ) args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(m): + async def handler(m) -> str: event.set() return "" - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("", queue)), + asyncio.create_task(br.publish("", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -601,12 +503,8 @@ async def handler(m): mock.end.assert_called_once() async def test_add_global_middleware( - self, - event: asyncio.Event, - queue: str, - mock: Mock, - raw_broker, - ): + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: class mid(BaseMiddleware): # noqa: N801 async def on_receive(self): mock.start(self.msg) @@ -616,13 +514,13 @@ async def after_processed(self, exc_type, exc_val, exc_tb): mock.end() return await super().after_processed(exc_type, exc_val, exc_tb) - broker = self.broker_class() + broker = self.get_broker() # already registered subscriber args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(m): + async def handler(m) -> str: event.set() return "" @@ -635,18 +533,16 @@ async def handler(m): args2, kwargs2 = self.get_subscriber_params(queue + "1") @broker.subscriber(*args2, **kwargs2) - async def handler2(m): + async def handler2(m) -> str: event2.set() return "" - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("", queue)), - asyncio.create_task(broker.publish("", f"{queue}1")), + asyncio.create_task(br.publish("", queue)), + asyncio.create_task(br.publish("", f"{queue}1")), asyncio.create_task(event.wait()), asyncio.create_task(event2.wait()), ), @@ -654,17 +550,18 @@ async def handler2(m): ) assert event.is_set() - assert mock.start.call_count == 2 - assert mock.end.call_count == 2 + assert mock.start.call_count == 2, mock.start.call_count + assert mock.end.call_count == 2, mock.end.call_count - async def test_patch_publish(self, queue: str, mock: Mock, event, raw_broker): + async def test_patch_publish( + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: class Mid(BaseMiddleware): - async def on_publish(self, msg: str, *args, **kwargs) -> str: - return msg * 2 + async def on_publish(self, msg: PublishCommand) -> PublishCommand: + msg.body *= 2 + return msg - broker = self.broker_class( - middlewares=(Mid,), - ) + broker = self.get_broker(middlewares=(Mid,)) args, kwargs = self.get_subscriber_params(queue) @@ -675,20 +572,16 @@ async def handler(m): args2, kwargs2 = self.get_subscriber_params(queue + "r") @broker.subscriber(*args2, **kwargs2) - async def handler_resp(m): + async def handler_resp(m) -> None: mock(m) event.set() - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task( - broker.publish("r", queue, reply_to=queue + "r") - ), + asyncio.create_task(br.publish("r", queue, reply_to=queue + "r")), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -698,25 +591,20 @@ async def handler_resp(m): mock.assert_called_once_with("rrrr") async def test_global_publisher_middleware( - self, - event: asyncio.Event, - queue: str, - mock: Mock, - raw_broker, - ): + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: class Mid(BaseMiddleware): - async def on_publish(self, msg: str, *args, **kwargs) -> str: - data = msg * 2 - assert args or kwargs - mock.enter(data) - return data + async def on_publish(self, msg: PublishCommand) -> PublishCommand: + msg.body *= 2 + mock.enter(msg.body) + return msg - async def after_publish(self, *args, **kwargs): + async def after_publish(self, *args, **kwargs) -> None: mock.end() if mock.end.call_count > 2: event.set() - broker = self.broker_class(middlewares=(Mid,)) + broker = self.get_broker(middlewares=(Mid,)) args, kwargs = self.get_subscriber_params(queue) @@ -727,13 +615,11 @@ async def handler(m): mock.inner(m) return m - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("1", queue)), + asyncio.create_task(br.publish("1", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -746,29 +632,18 @@ async def handler(m): assert mock.end.call_count == 3 -@pytest.mark.asyncio +@pytest.mark.asyncio() class ExceptionMiddlewareTestcase(BaseTestcaseConfig): - broker_class: Type[BrokerUsecase] - - @pytest.fixture - def raw_broker(self): - return None - - def patch_broker( - self, raw_broker: BrokerUsecase, broker: BrokerUsecase - ) -> BrokerUsecase: - return broker - async def test_exception_middleware_default_msg( - self, event: asyncio.Event, queue: str, mock: Mock, raw_broker - ): + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: mid = ExceptionMiddleware() @mid.add_handler(ValueError, publish=True) - async def value_error_handler(exc): + async def value_error_handler(exc) -> str: return "value" - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(apply_types=True, middlewares=(mid,)) args, kwargs = self.get_subscriber_params(queue) @@ -780,17 +655,15 @@ async def subscriber1(m): args, kwargs = self.get_subscriber_params(queue + "1") @broker.subscriber(*args, **kwargs) - async def subscriber2(msg=Context("message")): + async def subscriber2(msg=Context("message")) -> None: mock(await msg.decode()) event.set() - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("", queue)), + asyncio.create_task(br.publish("", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -801,18 +674,16 @@ async def subscriber2(msg=Context("message")): mock.assert_called_once_with("value") async def test_exception_middleware_skip_msg( - self, event: asyncio.Event, queue: str, mock: Mock, raw_broker - ): + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: mid = ExceptionMiddleware() @mid.add_handler(ValueError, publish=True) async def value_error_handler(exc): event.set() - raise SkipMessage() + raise SkipMessage - broker = self.broker_class( - middlewares=(mid,), - ) + broker = self.get_broker(middlewares=(mid,)) args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) @@ -823,16 +694,14 @@ async def subscriber1(m): args2, kwargs2 = self.get_subscriber_params(queue + "1") @broker.subscriber(*args2, **kwargs2) - async def subscriber2(msg=Context("message")): + async def subscriber2(msg=Context("message")) -> None: mock(await msg.decode()) - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("", queue)), + asyncio.create_task(br.publish("", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -842,17 +711,15 @@ async def subscriber2(msg=Context("message")): assert mock.call_count == 0 async def test_exception_middleware_do_not_catch_skip_msg( - self, event: asyncio.Event, queue: str, mock: Mock, raw_broker - ): + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: mid = ExceptionMiddleware() @mid.add_handler(Exception) - async def value_error_handler(exc): + async def value_error_handler(exc) -> None: mock() - broker = self.broker_class( - middlewares=(mid,), - ) + broker = self.get_broker(middlewares=(mid,)) args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) @@ -860,13 +727,11 @@ async def subscriber(m): event.set() raise SkipMessage - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("", queue)), + asyncio.create_task(br.publish("", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -877,8 +742,8 @@ async def subscriber(m): assert mock.call_count == 0 async def test_exception_middleware_reraise( - self, event: asyncio.Event, queue: str, mock: Mock, raw_broker - ): + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: mid = ExceptionMiddleware() @mid.add_handler(ValueError, publish=True) @@ -886,9 +751,7 @@ async def value_error_handler(exc): event.set() raise exc - broker = self.broker_class( - middlewares=(mid,), - ) + broker = self.get_broker(middlewares=(mid,)) args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) @@ -899,16 +762,14 @@ async def subscriber1(m): args2, kwargs2 = self.get_subscriber_params(queue + "1") @broker.subscriber(*args2, **kwargs2) - async def subscriber2(msg=Context("message")): + async def subscriber2(msg=Context("message")) -> None: mock(await msg.decode()) - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("", queue)), + asyncio.create_task(br.publish("", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -918,21 +779,19 @@ async def subscriber2(msg=Context("message")): assert mock.call_count == 0 async def test_exception_middleware_different_handler( - self, event: asyncio.Event, queue: str, mock: Mock, raw_broker - ): + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: mid = ExceptionMiddleware() @mid.add_handler(ZeroDivisionError, publish=True) - async def zero_error_handler(exc): + async def zero_error_handler(exc) -> str: return "zero" @mid.add_handler(ValueError, publish=True) - async def value_error_handler(exc): + async def value_error_handler(exc) -> str: return "value" - broker = self.broker_class( - middlewares=(mid,), - ) + broker = self.get_broker(apply_types=True, middlewares=(mid,)) args, kwargs = self.get_subscriber_params(queue) publisher = broker.publisher(queue + "2") @@ -952,19 +811,17 @@ async def subscriber2(m): args3, kwargs3 = self.get_subscriber_params(queue + "2") @broker.subscriber(*args3, **kwargs3) - async def subscriber3(msg=Context("message")): + async def subscriber3(msg=Context("message")) -> None: mock(await msg.decode()) if mock.call_count > 1: event.set() - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("", queue)), - asyncio.create_task(broker.publish("", queue + "1")), + asyncio.create_task(br.publish("", queue)), + asyncio.create_task(br.publish("", queue + "1")), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -974,22 +831,22 @@ async def subscriber3(msg=Context("message")): assert mock.call_count == 2 mock.assert_has_calls([call("zero"), call("value")], any_order=True) - async def test_exception_middleware_init_handler_same(self): + async def test_exception_middleware_init_handler_same(self) -> None: mid1 = ExceptionMiddleware() @mid1.add_handler(ValueError) - async def value_error_handler(exc): + async def value_error_handler(exc) -> str: return "value" mid2 = ExceptionMiddleware(handlers={ValueError: value_error_handler}) assert [x[0] for x in mid1._handlers] == [x[0] for x in mid2._handlers] - async def test_exception_middleware_init_publish_handler_same(self): + async def test_exception_middleware_init_publish_handler_same(self) -> None: mid1 = ExceptionMiddleware() @mid1.add_handler(ValueError, publish=True) - async def value_error_handler(exc): + async def value_error_handler(exc) -> str: return "value" mid2 = ExceptionMiddleware(publish_handlers={ValueError: value_error_handler}) @@ -999,8 +856,8 @@ async def value_error_handler(exc): ] async def test_exception_middleware_decoder_error( - self, event: asyncio.Event, queue: str, mock: Mock, raw_broker - ): + self, queue: str, mock: MagicMock, event: asyncio.Event + ) -> None: async def decoder( msg, original_decoder, @@ -1010,13 +867,10 @@ async def decoder( mid = ExceptionMiddleware() @mid.add_handler(ValueError) - async def value_error_handler(exc): + async def value_error_handler(exc) -> None: event.set() - broker = self.broker_class( - middlewares=(mid,), - decoder=decoder, - ) + broker = self.get_broker(middlewares=(mid,), decoder=decoder) args, kwargs = self.get_subscriber_params(queue) @@ -1024,13 +878,11 @@ async def value_error_handler(exc): async def subscriber1(m): raise ZeroDivisionError - broker = self.patch_broker(raw_broker, broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish("", queue)), + asyncio.create_task(br.publish("", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, diff --git a/tests/brokers/base/parser.py b/tests/brokers/base/parser.py index c4a47683c3..074d4115ff 100644 --- a/tests/brokers/base/parser.py +++ b/tests/brokers/base/parser.py @@ -1,35 +1,21 @@ import asyncio -from typing import Type -from unittest.mock import Mock +from unittest.mock import MagicMock import pytest -from faststream.broker.core.usecase import BrokerUsecase - from .basic import BaseTestcaseConfig -@pytest.mark.asyncio +@pytest.mark.asyncio() class LocalCustomParserTestcase(BaseTestcaseConfig): - broker_class: Type[BrokerUsecase] - - @pytest.fixture - def raw_broker(self): - return None - - def patch_broker( - self, raw_broker: BrokerUsecase, broker: BrokerUsecase - ) -> BrokerUsecase: - return broker - async def test_local_parser( self, - mock: Mock, + mock: MagicMock, queue: str, - raw_broker, - event: asyncio.Event, - ): - broker = self.broker_class() + ) -> None: + event = asyncio.Event() + + broker = self.get_broker() async def custom_parser(msg, original): msg = await original(msg) @@ -39,16 +25,15 @@ async def custom_parser(msg, original): args, kwargs = self.get_subscriber_params(queue, parser=custom_parser) @broker.subscriber(*args, **kwargs) - async def handle(m): + async def handle(m) -> None: event.set() - broker = self.patch_broker(raw_broker, broker) - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish(b"hello", queue)), + asyncio.create_task(br.publish(b"hello", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -59,12 +44,12 @@ async def handle(m): async def test_local_sync_decoder( self, - mock: Mock, + mock: MagicMock, queue: str, - raw_broker, - event: asyncio.Event, - ): - broker = self.broker_class() + ) -> None: + event = asyncio.Event() + + broker = self.get_broker() def custom_decoder(msg): mock(msg.body) @@ -73,16 +58,15 @@ def custom_decoder(msg): args, kwargs = self.get_subscriber_params(queue, decoder=custom_decoder) @broker.subscriber(*args, **kwargs) - async def handle(m): + async def handle(m) -> None: event.set() - broker = self.patch_broker(raw_broker, broker) - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish(b"hello", queue)), + asyncio.create_task(br.publish(b"hello", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -93,30 +77,29 @@ async def handle(m): async def test_global_sync_decoder( self, - mock: Mock, + mock: MagicMock, queue: str, - raw_broker, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + def custom_decoder(msg): mock(msg.body) return msg - broker = self.broker_class(decoder=custom_decoder) + broker = self.get_broker(decoder=custom_decoder) args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handle(m): + async def handle(m) -> None: event.set() - broker = self.patch_broker(raw_broker, broker) - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish(b"hello", queue)), + asyncio.create_task(br.publish(b"hello", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -127,13 +110,13 @@ async def handle(m): async def test_local_parser_no_share_between_subscribers( self, - event: asyncio.Event, - mock: Mock, + mock: MagicMock, queue: str, - raw_broker, - ): + ) -> None: + event = asyncio.Event() + event2 = asyncio.Event() - broker = self.broker_class() + broker = self.get_broker() async def custom_parser(msg, original): msg = await original(msg) @@ -145,20 +128,19 @@ async def custom_parser(msg, original): @broker.subscriber(*args, **kwargs) @broker.subscriber(*args2, **kwargs2) - async def handle(m): + async def handle(m) -> None: if event.is_set(): event2.set() else: event.set() - broker = self.patch_broker(raw_broker, broker) - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish(b"hello", queue)), - asyncio.create_task(broker.publish(b"hello", queue + "1")), + asyncio.create_task(br.publish(b"hello", queue)), + asyncio.create_task(br.publish(b"hello", queue + "1")), asyncio.create_task(event.wait()), asyncio.create_task(event2.wait()), ), @@ -171,19 +153,18 @@ async def handle(m): async def test_local_parser_no_share_between_handlers( self, - mock: Mock, + mock: MagicMock, queue: str, - raw_broker, - event: asyncio.Event, - ): - broker = self.broker_class() + ) -> None: + event = asyncio.Event() - args, kwargs = self.get_subscriber_params( - queue, filter=lambda m: m.content_type == "application/json" - ) + broker = self.get_broker() - @broker.subscriber(*args, **kwargs) - async def handle(m): + args, kwargs = self.get_subscriber_params(queue) + sub = broker.subscriber(*args, **kwargs) + + @sub(filter=lambda m: m.content_type == "application/json") + async def handle(m) -> None: event.set() event2 = asyncio.Event() @@ -193,20 +174,17 @@ async def custom_parser(msg, original): mock(msg.body) return msg - args2, kwargs2 = self.get_subscriber_params(queue, parser=custom_parser) - - @broker.subscriber(*args2, **kwargs2) - async def handle2(m): + @sub(parser=custom_parser) + async def handle2(m) -> None: event2.set() - broker = self.patch_broker(raw_broker, broker) - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish({"msg": "hello"}, queue)), - asyncio.create_task(broker.publish(b"hello", queue)), + asyncio.create_task(br.publish({"msg": "hello"}, queue)), + asyncio.create_task(br.publish(b"hello", queue)), asyncio.create_task(event.wait()), asyncio.create_task(event2.wait()), ), @@ -221,31 +199,30 @@ async def handle2(m): class CustomParserTestcase(LocalCustomParserTestcase): async def test_global_parser( self, - mock: Mock, + mock: MagicMock, queue: str, - raw_broker, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + async def custom_parser(msg, original): msg = await original(msg) mock(msg.body) return msg - broker = self.broker_class(parser=custom_parser) + broker = self.get_broker(parser=custom_parser) args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handle(m): + async def handle(m) -> None: event.set() - broker = self.patch_broker(raw_broker, broker) - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(broker.publish(b"hello", queue)), + asyncio.create_task(br.publish(b"hello", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, diff --git a/tests/brokers/base/publish.py b/tests/brokers/base/publish.py index 79c538a397..de2240198a 100644 --- a/tests/brokers/base/publish.py +++ b/tests/brokers/base/publish.py @@ -1,17 +1,17 @@ import asyncio -from abc import abstractmethod +from contextlib import suppress from dataclasses import asdict, dataclass -from datetime import datetime -from typing import Any, Dict, List, Tuple -from unittest.mock import Mock +from datetime import datetime, timezone +from typing import Any +from unittest.mock import MagicMock import anyio import pytest from pydantic import BaseModel from faststream import BaseMiddleware, Context, Response -from faststream._compat import dump_json, model_to_json -from faststream.broker.core.usecase import BrokerUsecase +from faststream._internal._compat import dump_json, model_to_json +from faststream.exceptions import SubscriberNotFound from .basic import BaseTestcaseConfig @@ -25,75 +25,96 @@ class SimpleDataclass: r: str -now = datetime.now() +now = datetime.now(timezone.utc) + +parametrized = ( + pytest.param( + "hello", + str, + "hello", + id="str->str", + ), + pytest.param( + b"hello", + bytes, + b"hello", + id="bytes->bytes", + ), + pytest.param( + 1, + int, + 1, + id="int->int", + ), + pytest.param( + 1.0, + float, + 1.0, + id="float->float", + ), + pytest.param( + 1, + float, + 1.0, + id="int->float", + ), + pytest.param( + False, + bool, + False, + id="bool->bool", + ), + pytest.param( + {"m": 1}, + dict[str, int], + {"m": 1}, + id="dict->dict", + ), + pytest.param( + [1, 2, 3], + list[int], + [1, 2, 3], + id="list->list", + ), + pytest.param( + now, + datetime, + now, + id="datetime->datetime", + ), + pytest.param( + dump_json(asdict(SimpleDataclass(r="hello!"))), + SimpleDataclass, + SimpleDataclass(r="hello!"), + id="bytes->dataclass", + ), + pytest.param( + SimpleDataclass(r="hello!"), + SimpleDataclass, + SimpleDataclass(r="hello!"), + id="dataclass->dataclass", + ), + pytest.param( + SimpleDataclass(r="hello!"), + dict, + {"r": "hello!"}, + id="dataclass->dict", + ), + pytest.param( + {"r": "hello!"}, + SimpleDataclass, + SimpleDataclass(r="hello!"), + id="dict->dataclass", + ), +) class BrokerPublishTestcase(BaseTestcaseConfig): - @abstractmethod - def get_broker(self, apply_types: bool = False) -> BrokerUsecase[Any, Any]: - raise NotImplementedError - - def patch_broker(self, broker: BrokerUsecase[Any, Any]) -> BrokerUsecase[Any, Any]: - return broker - - @pytest.mark.asyncio + @pytest.mark.asyncio() @pytest.mark.parametrize( ("message", "message_type", "expected_message"), - ( # noqa: PT007 - pytest.param( - "hello", - str, - "hello", - id="str->str", - ), - pytest.param( - b"hello", - bytes, - b"hello", - id="bytes->bytes", - ), - pytest.param( - 1, - int, - 1, - id="int->int", - ), - pytest.param( - 1.0, - float, - 1.0, - id="float->float", - ), - pytest.param( - 1, - float, - 1.0, - id="int->float", - ), - pytest.param( - False, - bool, - False, - id="bool->bool", - ), - pytest.param( - {"m": 1}, - Dict[str, int], - {"m": 1}, - id="dict->dict", - ), - pytest.param( - [1, 2, 3], - List[int], - [1, 2, 3], - id="list->list", - ), - pytest.param( - now, - datetime, - now, - id="datetime->datetime", - ), + ( + *parametrized, pytest.param( model_to_json(SimpleModel(r="hello!")).encode(), SimpleModel, @@ -118,47 +139,24 @@ def patch_broker(self, broker: BrokerUsecase[Any, Any]) -> BrokerUsecase[Any, An SimpleModel(r="hello!"), id="dict->model", ), - pytest.param( - dump_json(asdict(SimpleDataclass(r="hello!"))), - SimpleDataclass, - SimpleDataclass(r="hello!"), - id="bytes->dataclass", - ), - pytest.param( - SimpleDataclass(r="hello!"), - SimpleDataclass, - SimpleDataclass(r="hello!"), - id="dataclass->dataclass", - ), - pytest.param( - SimpleDataclass(r="hello!"), - dict, - {"r": "hello!"}, - id="dataclass->dict", - ), - pytest.param( - {"r": "hello!"}, - SimpleDataclass, - SimpleDataclass(r="hello!"), - id="dict->dataclass", - ), ), ) async def test_serialize( self, queue: str, - message, - message_type, - expected_message, - event: asyncio.Event, - mock: Mock, - ): + message: Any, + message_type: Any, + expected_message: Any, + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue) @pub_broker.subscriber(*args, **kwargs) - async def handler(m: message_type): + async def handler(m: message_type) -> None: event.set() mock(m) @@ -173,16 +171,16 @@ async def handler(m: message_type): timeout=self.timeout, ) - assert event.is_set() mock.assert_called_with(expected_message) - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_response( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue) @@ -195,7 +193,7 @@ async def m(): args2, kwargs2 = self.get_subscriber_params(queue + "1") @pub_broker.subscriber(*args2, **kwargs2) - async def m_next(msg=Context("message")): + async def m_next(msg=Context("message")) -> None: event.set() mock( body=msg.body, @@ -213,26 +211,26 @@ async def m_next(msg=Context("message")): timeout=self.timeout, ) - assert event.is_set() mock.assert_called_with( body=b"1", correlation_id="1", headers="1", ) - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_unwrap_dict( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue) @pub_broker.subscriber(*args, **kwargs) - async def m(a: int, b: int): + async def m(a: int, b: int) -> None: event.set() mock({"a": a, "b": b}) @@ -246,27 +244,27 @@ async def m(a: int, b: int): timeout=self.timeout, ) - assert event.is_set() mock.assert_called_with( { "a": 1, "b": 1, - } + }, ) - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_unwrap_list( self, - mock: Mock, + mock: MagicMock, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue) @pub_broker.subscriber(*args, **kwargs) - async def m(a: int, b: int, *args: Tuple[int, ...]): + async def m(a: int, b: int, *args: tuple[int, ...]) -> None: event.set() mock({"a": a, "b": b, "args": args}) @@ -280,29 +278,29 @@ async def m(a: int, b: int, *args: Tuple[int, ...]): timeout=self.timeout, ) - assert event.is_set() mock.assert_called_with({"a": 1, "b": 1, "args": (2, 3)}) - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_base_publisher( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue) @pub_broker.subscriber(*args, **kwargs) @pub_broker.publisher(queue + "resp") - async def m(): + async def m() -> str: return "" args2, kwargs2 = self.get_subscriber_params(queue + "resp") @pub_broker.subscriber(*args2, **kwargs2) - async def resp(msg): + async def resp(msg) -> None: event.set() mock(msg) @@ -319,13 +317,14 @@ async def resp(msg): assert event.is_set() mock.assert_called_once_with("") - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_publisher_object( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) publisher = pub_broker.publisher(queue + "resp") @@ -334,13 +333,13 @@ async def test_publisher_object( @publisher @pub_broker.subscriber(*args, **kwargs) - async def m(): + async def m() -> str: return "" args, kwargs = self.get_subscriber_params(queue + "resp") @pub_broker.subscriber(*args, **kwargs) - async def resp(msg): + async def resp(msg) -> None: event.set() mock(msg) @@ -354,16 +353,16 @@ async def resp(msg): timeout=self.timeout, ) - assert event.is_set() mock.assert_called_once_with("") - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_publish_manual( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) publisher = pub_broker.publisher(queue + "resp") @@ -371,13 +370,13 @@ async def test_publish_manual( args, kwargs = self.get_subscriber_params(queue) @pub_broker.subscriber(*args, **kwargs) - async def m(): + async def m() -> None: await publisher.publish("") args2, kwargs2 = self.get_subscriber_params(queue + "resp") @pub_broker.subscriber(*args2, **kwargs2) - async def resp(msg): + async def resp(msg) -> None: event.set() mock(msg) @@ -391,15 +390,14 @@ async def resp(msg): timeout=self.timeout, ) - assert event.is_set() mock.assert_called_once_with("") - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_multiple_publishers( self, queue: str, - mock: Mock, - ): + mock: MagicMock, + ) -> None: pub_broker = self.get_broker(apply_types=True) event = anyio.Event() @@ -410,20 +408,20 @@ async def test_multiple_publishers( @pub_broker.publisher(queue + "resp2") @pub_broker.subscriber(*args, **kwargs) @pub_broker.publisher(queue + "resp") - async def m(): + async def m() -> str: return "" args2, kwargs2 = self.get_subscriber_params(queue + "resp") @pub_broker.subscriber(*args2, **kwargs2) - async def resp(msg): + async def resp(msg) -> None: event.set() mock.resp1(msg) args3, kwargs3 = self.get_subscriber_params(queue + "resp2") @pub_broker.subscriber(*args3, **kwargs3) - async def resp2(msg): + async def resp2(msg) -> None: event2.set() mock.resp2(msg) @@ -438,17 +436,15 @@ async def resp2(msg): timeout=self.timeout, ) - assert event.is_set() - assert event2.is_set() mock.resp1.assert_called_once_with("") mock.resp2.assert_called_once_with("") - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_reusable_publishers( self, queue: str, - mock: Mock, - ): + mock: MagicMock, + ) -> None: pub_broker = self.get_broker(apply_types=True) consume = anyio.Event() @@ -460,20 +456,20 @@ async def test_reusable_publishers( @pub @pub_broker.subscriber(*args, **kwargs) - async def m(): + async def m() -> str: return "" args2, kwargs2 = self.get_subscriber_params(queue + "2") @pub @pub_broker.subscriber(*args2, **kwargs2) - async def m2(): + async def m2() -> str: return "" args3, kwargs3 = self.get_subscriber_params(queue + "resp") @pub_broker.subscriber(*args3, **kwargs3) - async def resp(): + async def resp() -> None: if not consume.is_set(): consume.set() else: @@ -492,23 +488,22 @@ async def resp(): timeout=self.timeout, ) - assert consume2.is_set() - assert consume.is_set() assert mock.call_count == 2 - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_reply_to( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue + "reply") @pub_broker.subscriber(*args, **kwargs) - async def reply_handler(m): + async def reply_handler(m) -> None: event.set() mock(m) @@ -524,23 +519,23 @@ async def handler(m): await asyncio.wait( ( asyncio.create_task( - br.publish("Hello!", queue, reply_to=queue + "reply") + br.publish("Hello!", queue, reply_to=queue + "reply"), ), asyncio.create_task(event.wait()), ), timeout=self.timeout, ) - assert event.is_set() mock.assert_called_with("Hello!") - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_no_reply( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + class Mid(BaseMiddleware): async def after_processed(self, *args: Any, **kwargs: Any): event.set() @@ -553,7 +548,7 @@ async def after_processed(self, *args: Any, **kwargs: Any): args, kwargs = self.get_subscriber_params(queue + "reply") @pub_broker.subscriber(*args, **kwargs) - async def reply_handler(m): + async def reply_handler(m) -> None: mock(m) args2, kwargs2 = self.get_subscriber_params(queue, no_reply=True) @@ -568,35 +563,37 @@ async def handler(m): await asyncio.wait( ( asyncio.create_task( - br.publish("Hello!", queue, reply_to=queue + "reply") + br.publish("Hello!", queue, reply_to=queue + "reply"), ), asyncio.create_task(event.wait()), ), timeout=self.timeout, ) - assert event.is_set() assert not mock.called - @pytest.mark.asyncio - async def test_publisher_after_connect(self, queue: str): + @pytest.mark.asyncio() + async def test_publisher_after_connect(self, queue: str) -> None: async with self.patch_broker(self.get_broker()) as br: # Should pass without error - await br.publisher(queue).publish(None) + # suppress TestClient error due where is no suitable subscriber + with suppress(SubscriberNotFound): + await br.publisher(queue).publish(None) - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_publisher_after_start( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue) @pub_broker.subscriber(*args, **kwargs) - async def handler(m): + async def handler(m) -> None: event.set() mock(m) @@ -613,5 +610,4 @@ async def handler(m): timeout=self.timeout, ) - assert event.is_set() mock.assert_called_with("Hello!") diff --git a/tests/brokers/base/publish_command.py b/tests/brokers/base/publish_command.py new file mode 100644 index 0000000000..8624ec6c7d --- /dev/null +++ b/tests/brokers/base/publish_command.py @@ -0,0 +1,73 @@ +from typing import Any + +import pytest + +from faststream import Response +from faststream.response import ensure_response +from faststream.response.response import ( + BatchPublishCommand, + PublishCommand, +) + + +class BasePublishCommandTestcase: + publish_command_cls: type[PublishCommand] + + def test_simple_reponse(self) -> None: + response = ensure_response(1) + cmd = self.publish_command_cls.from_cmd(response.as_publish_command()) + assert cmd.body == 1 + + def test_base_response_class(self) -> None: + response = ensure_response(Response(body=1, headers={"1": 1})) + cmd = self.publish_command_cls.from_cmd(response.as_publish_command()) + assert cmd.body == 1 + assert cmd.headers == {"1": 1} + + +class BatchPublishCommandTestcase(BasePublishCommandTestcase): + publish_command_cls: type[BatchPublishCommand] + + @pytest.mark.parametrize( + ("data", "expected_body"), + ( + pytest.param(None, (), id="None Response"), + pytest.param((), (), id="Empty Sequence"), + pytest.param("123", ("123",), id="String Response"), + pytest.param("", ("",), id="Empty String Response"), + pytest.param(b"", (b"",), id="Empty Bytes Response"), + pytest.param([1, 2, 3], (1, 2, 3), id="Sequence Data"), + pytest.param( + [0, 1, 2], (0, 1, 2), id="Sequence Data with False first element" + ), + ), + ) + def test_batch_response(self, data: Any, expected_body: Any) -> None: + response = ensure_response(data) + cmd = self.publish_command_cls.from_cmd( + response.as_publish_command(), + batch=True, + ) + assert cmd.batch_bodies == expected_body + + def test_batch_bodies_setter(self) -> None: + response = ensure_response(None) + cmd = self.publish_command_cls.from_cmd( + response.as_publish_command(), batch=True + ) + cmd.batch_bodies = (1, 2, 3) + + assert cmd.batch_bodies == (1, 2, 3) + assert cmd.body == 1 + assert cmd.extra_bodies == (2, 3) + + def test_batch_bodies_empty_setter(self) -> None: + response = ensure_response((1, 2, 3)) + cmd = self.publish_command_cls.from_cmd( + response.as_publish_command(), batch=True + ) + cmd.batch_bodies = () + + assert cmd.batch_bodies == () + assert cmd.body is None + assert cmd.extra_bodies == () diff --git a/tests/brokers/base/requests.py b/tests/brokers/base/requests.py index 78dcdcb58b..16414f47d3 100644 --- a/tests/brokers/base/requests.py +++ b/tests/brokers/base/requests.py @@ -1,3 +1,5 @@ +import asyncio + import anyio import pytest @@ -8,42 +10,36 @@ class RequestsTestcase(BaseTestcaseConfig): def get_middleware(self, **kwargs): raise NotImplementedError - def get_broker(self, **kwargs): - raise NotImplementedError - def get_router(self, **kwargs): raise NotImplementedError - def patch_broker(self, broker, **kwargs): - return broker - - async def test_request_timeout(self, queue: str): + async def test_request_timeout(self, queue: str) -> None: broker = self.get_broker() args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(msg): - await anyio.sleep(1.0) + async def handler(msg) -> str: + await anyio.sleep(0.01) return "Response" async with self.patch_broker(broker): await broker.start() - with pytest.raises(TimeoutError): + with pytest.raises((TimeoutError, asyncio.TimeoutError)): await broker.request( None, queue, timeout=1e-24, ) - async def test_broker_base_request(self, queue: str): + async def test_broker_base_request(self, queue: str) -> None: broker = self.get_broker() args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg) -> str: return "Response" async with self.patch_broker(broker): @@ -59,7 +55,7 @@ async def handler(msg): assert await response.decode() == "Response" assert response.correlation_id == "1", response.correlation_id - async def test_publisher_base_request(self, queue: str): + async def test_publisher_base_request(self, queue: str) -> None: broker = self.get_broker() publisher = broker.publisher(queue) @@ -67,7 +63,7 @@ async def test_publisher_base_request(self, queue: str): args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg) -> str: return "Response" async with self.patch_broker(broker): @@ -82,7 +78,7 @@ async def handler(msg): assert await response.decode() == "Response" assert response.correlation_id == "1", response.correlation_id - async def test_router_publisher_request(self, queue: str): + async def test_router_publisher_request(self, queue: str) -> None: router = self.get_router() publisher = router.publisher(queue) @@ -90,7 +86,7 @@ async def test_router_publisher_request(self, queue: str): args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg) -> str: return "Response" broker = self.get_broker() @@ -108,7 +104,7 @@ async def handler(msg): assert await response.decode() == "Response" assert response.correlation_id == "1", response.correlation_id - async def test_broker_request_respect_middleware(self, queue: str): + async def test_broker_request_respect_middleware(self, queue: str) -> None: broker = self.get_broker(middlewares=(self.get_middleware(),)) args, kwargs = self.get_subscriber_params(queue) @@ -128,7 +124,9 @@ async def handler(msg): assert await response.decode() == "x" * 2 * 2 * 2 * 2 - async def test_broker_publisher_request_respect_middleware(self, queue: str): + async def test_broker_publisher_request_respect_middleware( + self, queue: str + ) -> None: broker = self.get_broker(middlewares=(self.get_middleware(),)) publisher = broker.publisher(queue) @@ -149,7 +147,9 @@ async def handler(msg): assert await response.decode() == "x" * 2 * 2 * 2 * 2 - async def test_router_publisher_request_respect_middleware(self, queue: str): + async def test_router_publisher_request_respect_middleware( + self, queue: str + ) -> None: router = self.get_router(middlewares=(self.get_middleware(),)) publisher = router.publisher(queue) diff --git a/tests/brokers/base/router.py b/tests/brokers/base/router.py index 02d60cd578..9e269534f5 100644 --- a/tests/brokers/base/router.py +++ b/tests/brokers/base/router.py @@ -1,59 +1,78 @@ import asyncio -from typing import Type -from unittest.mock import Mock +from typing import Any +from unittest.mock import MagicMock import pytest -from faststream import BaseMiddleware, Depends -from faststream.broker.core.usecase import BrokerUsecase -from faststream.broker.router import ArgsContainer, BrokerRouter, SubscriberRoute -from faststream.types import AnyCallable +from faststream._internal.broker.router import ( + ArgsContainer, + BrokerRouter, + SubscriberRoute, +) from tests.brokers.base.middlewares import LocalMiddlewareTestcase from tests.brokers.base.parser import LocalCustomParserTestcase -@pytest.mark.asyncio +@pytest.mark.asyncio() class RouterTestcase( LocalMiddlewareTestcase, LocalCustomParserTestcase, ): - build_message: AnyCallable - route_class: Type[SubscriberRoute] - publisher_class: Type[ArgsContainer] + route_class: type[SubscriberRoute] + publisher_class: type[ArgsContainer] - def patch_broker(self, br: BrokerUsecase, router: BrokerRouter) -> BrokerUsecase: - br.include_router(router) - return br + def get_router(self, **kwargs: Any) -> BrokerRouter: + raise NotImplementedError - @pytest.fixture - def pub_broker(self, broker): - return broker - - @pytest.fixture - def raw_broker(self, pub_broker): - return pub_broker - - async def test_empty_prefix( + async def test_router_dynamic_objects( self, - router: BrokerRouter, - pub_broker: BrokerUsecase, queue: str, event: asyncio.Event, - ): + ) -> None: + nested_router = self.get_router() + router = self.get_router(routers=[nested_router]) + broker = self.get_broker(routers=[router]) + + def subscriber(m) -> None: + event.set() + + async with self.patch_broker(broker) as br: + await br.start() + + args, kwargs = self.get_subscriber_params(queue) + sub = nested_router.subscriber(*args, **kwargs) + sub(subscriber) + await sub.start() + + await asyncio.wait( + ( + asyncio.create_task(br.publish("Hi!", queue)), + asyncio.create_task(event.wait()), + ), + timeout=self.timeout, + ) + + assert event.is_set() + + async def test_empty_prefix(self, queue: str) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + router = self.get_router() + args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - def subscriber(m): + def subscriber(m) -> None: event.set() pub_broker.include_router(router) - - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", queue)), + asyncio.create_task(br.publish("hello", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -63,27 +82,27 @@ def subscriber(m): async def test_not_empty_prefix( self, - router: BrokerRouter, - pub_broker: BrokerUsecase, queue: str, - event: asyncio.Event, - ): - router.prefix = "test_" + ) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + + router = self.get_router(prefix="test_") args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - def subscriber(m): + def subscriber(m) -> None: event.set() pub_broker.include_router(router) - - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", f"test_{queue}")), + asyncio.create_task(br.publish("hello", f"test_{queue}")), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -91,27 +110,25 @@ def subscriber(m): assert event.is_set() - async def test_include_with_prefix( - self, - router: BrokerRouter, - pub_broker: BrokerUsecase, - queue: str, - event: asyncio.Event, - ): + async def test_include_with_prefix(self, queue: str) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + router = self.get_router() + args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - def subscriber(m): + def subscriber(m) -> None: event.set() pub_broker.include_router(router, prefix="test_") - - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", f"test_{queue}")), + asyncio.create_task(br.publish("hello", f"test_{queue}")), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -119,34 +136,32 @@ def subscriber(m): assert event.is_set() - async def test_empty_prefix_publisher( - self, - router: BrokerRouter, - pub_broker: BrokerUsecase, - queue: str, - event: asyncio.Event, - ): + async def test_empty_prefix_publisher(self, queue: str) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + router = self.get_router() + args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) @router.publisher(queue + "resp") - def subscriber(m): + def subscriber(m) -> str: return "hi" args2, kwargs2 = self.get_subscriber_params(queue + "resp") @router.subscriber(*args2, **kwargs2) - def response(m): + def response(m) -> None: event.set() pub_broker.include_router(router) - - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", queue)), + asyncio.create_task(br.publish("hello", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -156,34 +171,34 @@ def response(m): async def test_not_empty_prefix_publisher( self, - router: BrokerRouter, - pub_broker: BrokerUsecase, queue: str, - event: asyncio.Event, - ): - router.prefix = "test_" + ) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + + router = self.get_router(prefix="test_") args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) @router.publisher(queue + "resp") - def subscriber(m): + def subscriber(m) -> str: return "hi" args2, kwargs2 = self.get_subscriber_params(queue + "resp") @router.subscriber(*args2, **kwargs2) - def response(m): + def response(m) -> None: event.set() pub_broker.include_router(router) - - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", f"test_{queue}")), + asyncio.create_task(br.publish("hello", f"test_{queue}")), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -193,35 +208,35 @@ def response(m): async def test_manual_publisher( self, - router: BrokerRouter, - pub_broker: BrokerUsecase, queue: str, - event: asyncio.Event, - ): - router.prefix = "test_" + ) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + + router = self.get_router(prefix="test_") p = router.publisher(queue + "resp") args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - async def subscriber(m): + async def subscriber(m) -> None: await p.publish("resp") args2, kwargs2 = self.get_subscriber_params(queue + "resp") @router.subscriber(*args2, **kwargs2) - def response(m): + def response(m) -> None: event.set() pub_broker.include_router(router) - - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", f"test_{queue}")), + asyncio.create_task(br.publish("hello", f"test_{queue}")), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -229,31 +244,28 @@ def response(m): assert event.is_set() - async def test_delayed_handlers( - self, - event: asyncio.Event, - router: BrokerRouter, - queue: str, - pub_broker: BrokerUsecase, - ): - def response(m): + async def test_delayed_handlers(self, queue: str) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + + def response(m) -> None: event.set() args, kwargs = self.get_subscriber_params(queue) - r = type(router)( + router = self.get_router( prefix="test_", handlers=(self.route_class(response, *args, **kwargs),), ) - pub_broker.include_router(r) - - async with pub_broker: - await pub_broker.start() + pub_broker.include_router(router) + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", f"test_{queue}")), + asyncio.create_task(br.publish("hello", f"test_{queue}")), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -261,20 +273,17 @@ def response(m): assert event.is_set() - async def test_delayed_publishers( - self, - event: asyncio.Event, - router: BrokerRouter, - queue: str, - pub_broker: BrokerUsecase, - mock: Mock, - ): + async def test_delayed_publishers(self, queue: str, mock: MagicMock) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + def response(m): return m args, kwargs = self.get_subscriber_params(queue) - r = type(router)( + router = self.get_router( prefix="test_", handlers=( self.route_class( @@ -286,21 +295,21 @@ def response(m): ), ) - pub_broker.include_router(r) + pub_broker.include_router(router) args, kwargs = self.get_subscriber_params(f"test_{queue}1") @pub_broker.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg) -> None: mock(msg) event.set() - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", f"test_{queue}")), + asyncio.create_task(br.publish("hello", f"test_{queue}")), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -312,19 +321,20 @@ async def handler(msg): async def test_nested_routers_sub( self, - router: BrokerRouter, - pub_broker: BrokerUsecase, queue: str, - event: asyncio.Event, - mock: Mock, - ): - core_router = type(router)(prefix="test1_") - router.prefix = "test2_" + mock: MagicMock, + ) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + + core_router = self.get_router(prefix="test1_") + router = self.get_router(prefix="test2_") args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - def subscriber(m): + def subscriber(m) -> str: event.set() mock(m) return "hi" @@ -332,14 +342,12 @@ def subscriber(m): core_router.include_routers(router) pub_broker.include_routers(core_router) - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task( - pub_broker.publish("hello", f"test1_test2_{queue}") - ), + asyncio.create_task(br.publish("hello", f"test1_test2_{queue}")), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -350,40 +358,39 @@ def subscriber(m): async def test_nested_routers_pub( self, - router: BrokerRouter, - pub_broker: BrokerUsecase, queue: str, - event: asyncio.Event, - ): - core_router = type(router)(prefix="test1_") - router.prefix = "test2_" + ) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + + core_router = self.get_router(prefix="test1_") + router = self.get_router(prefix="test2_") args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) @router.publisher(queue + "resp") - def subscriber(m): + def subscriber(m) -> str: return "hi" args2, kwargs2 = self.get_subscriber_params( - "test1_" + "test2_" + queue + "resp" + "test1_" + "test2_" + queue + "resp", ) @pub_broker.subscriber(*args2, **kwargs2) - def response(m): + def response(m) -> None: event.set() core_router.include_routers(router) - pub_broker.include_routers(core_router) + pub_broker.include_router(core_router) - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task( - pub_broker.publish("hello", f"test1_test2_{queue}") - ), + asyncio.create_task(br.publish("hello", f"test1_test2_{queue}")), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -391,107 +398,15 @@ def response(m): assert event.is_set() - async def test_router_dependencies( - self, - router: BrokerRouter, - pub_broker: BrokerUsecase, - queue: str, - ): - router = type(router)(dependencies=(Depends(lambda: 1),)) - router2 = type(router)(dependencies=(Depends(lambda: 2),)) - - args, kwargs = self.get_subscriber_params( - queue, dependencies=(Depends(lambda: 3),) - ) - - @router2.subscriber(*args, **kwargs) - def subscriber(): ... - - router.include_router(router2) - pub_broker.include_routers(router) - - sub = next(iter(pub_broker._subscribers.values())) - assert len((*sub._broker_dependencies, *sub.calls[0].dependencies)) == 3 - - async def test_router_include_with_dependencies( - self, - router: BrokerRouter, - pub_broker: BrokerUsecase, - queue: str, - ): - router2 = type(router)() - - args, kwargs = self.get_subscriber_params( - queue, - dependencies=(Depends(lambda: 3),), - ) - - @router2.subscriber(*args, **kwargs) - def subscriber(): ... - - router.include_router(router2, dependencies=(Depends(lambda: 2),)) - pub_broker.include_router(router, dependencies=(Depends(lambda: 1),)) - - sub = next(iter(pub_broker._subscribers.values())) - dependencies = (*sub._broker_dependencies, *sub.calls[0].dependencies) - assert len(dependencies) == 3, dependencies - - async def test_router_middlewares( - self, - router: BrokerRouter, - pub_broker: BrokerUsecase, - queue: str, - ): - router = type(router)(middlewares=(BaseMiddleware,)) - router2 = type(router)(middlewares=(BaseMiddleware,)) - - args, kwargs = self.get_subscriber_params(queue, middlewares=(3,)) - - @router2.subscriber(*args, **kwargs) - @router2.publisher(queue, middlewares=(3,)) - def subscriber(): ... - - router.include_router(router2) - pub_broker.include_routers(router) - - sub = next(iter(pub_broker._subscribers.values())) - publisher = next(iter(pub_broker._publishers.values())) - - assert len((*sub._broker_middlewares, *sub.calls[0].item_middlewares)) == 3 - assert len((*publisher._broker_middlewares, *publisher._middlewares)) == 3 - - async def test_router_include_with_middlewares( + async def test_router_parser( self, - router: BrokerRouter, - pub_broker: BrokerUsecase, queue: str, - ): - router2 = type(router)() - - args, kwargs = self.get_subscriber_params(queue, middlewares=(3,)) - - @router2.subscriber(*args, **kwargs) - @router2.publisher(queue, middlewares=(3,)) - def subscriber(): ... + mock: MagicMock, + ) -> None: + event = asyncio.Event() - router.include_router(router2, middlewares=(BaseMiddleware,)) - pub_broker.include_router(router, middlewares=(BaseMiddleware,)) + pub_broker = self.get_broker() - sub = next(iter(pub_broker._subscribers.values())) - publisher = next(iter(pub_broker._publishers.values())) - - sub_middlewares = (*sub._broker_middlewares, *sub.calls[0].item_middlewares) - assert len(sub_middlewares) == 3, sub_middlewares - assert len((*publisher._broker_middlewares, *publisher._middlewares)) == 3 - - async def test_router_parser( - self, - router: BrokerRouter, - pub_broker: BrokerUsecase, - queue: str, - event: asyncio.Event, - mock: Mock, - ): async def parser(msg, original): mock.parser() return await original(msg) @@ -500,25 +415,21 @@ async def decoder(msg, original): mock.decoder() return await original(msg) - router = type(router)( - parser=parser, - decoder=decoder, - ) + router = self.get_router(parser=parser, decoder=decoder) args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - def subscriber(s): + def subscriber(s) -> None: event.set() - pub_broker.include_routers(router) - - async with pub_broker: - await pub_broker.start() + pub_broker.include_router(router) + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", queue)), + asyncio.create_task(br.publish("hello", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -528,14 +439,11 @@ def subscriber(s): mock.parser.assert_called_once() mock.decoder.assert_called_once() - async def test_router_parser_override( - self, - router: BrokerRouter, - pub_broker: BrokerUsecase, - queue: str, - event: asyncio.Event, - mock: Mock, - ): + async def test_router_parser_override(self, queue: str, mock: MagicMock) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + async def global_parser(msg, original): # pragma: no cover mock() return await original(msg) @@ -552,7 +460,7 @@ async def decoder(msg, original): mock.decoder() return await original(msg) - router = type(router)( + router = self.get_router( parser=global_parser, decoder=global_decoder, ) @@ -560,17 +468,16 @@ async def decoder(msg, original): args, kwargs = self.get_subscriber_params(queue, parser=parser, decoder=decoder) @router.subscriber(*args, **kwargs) - def subscriber(s): + def subscriber(s) -> None: event.set() - pub_broker.include_routers(router) - - async with pub_broker: - await pub_broker.start() + pub_broker.include_router(router) + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", queue)), + asyncio.create_task(br.publish("hello", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -581,38 +488,70 @@ def subscriber(s): mock.parser.assert_called_once() mock.decoder.assert_called_once() + async def test_router_in_init(self, queue: str) -> None: + event = asyncio.Event() + + args, kwargs = self.get_subscriber_params(queue) + router = self.get_router() + + @router.subscriber(*args, **kwargs) + def subscriber(m) -> None: + event.set() + + pub_broker = self.get_broker(routers=[router]) + + async with self.patch_broker(pub_broker) as br: + await br.start() + + await asyncio.wait( + ( + asyncio.create_task(br.publish("hello", queue)), + asyncio.create_task(event.wait()), + ), + timeout=self.timeout, + ) + + assert event.is_set() + + async def test_correct_include_router_with_same_name(self) -> None: + pub_broker = self.get_broker() + + router1 = self.get_router() + router2 = self.get_router() + + router1.publisher("l3") + router2.publisher("l3") + + pub_broker.include_routers(router2, router1) -@pytest.mark.asyncio + pub1 = router1.publishers[0] + pub2 = router2.publishers[0] + assert pub1._producer is pub2._producer + + +@pytest.mark.asyncio() class RouterLocalTestcase(RouterTestcase): - @pytest.fixture - def pub_broker(self, test_broker): - return test_broker + async def test_publisher_mock(self, queue: str, event: asyncio.Event) -> None: + pub_broker = self.get_broker() + router = self.get_router() - async def test_publisher_mock( - self, - router: BrokerRouter, - pub_broker: BrokerUsecase, - queue: str, - event: asyncio.Event, - ): pub = router.publisher(queue + "resp") args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) @pub - def subscriber(m): + def subscriber(m) -> str: event.set() return "hi" pub_broker.include_router(router) - - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", queue)), + asyncio.create_task(br.publish("hello", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -621,28 +560,26 @@ def subscriber(m): assert event.is_set() pub.mock.assert_called_with("hi") - async def test_subscriber_mock( - self, - router: BrokerRouter, - pub_broker: BrokerUsecase, - queue: str, - event: asyncio.Event, - ): + async def test_subscriber_mock(self, queue: str) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + router = self.get_router() + args, kwargs = self.get_subscriber_params(queue) @router.subscriber(*args, **kwargs) - def subscriber(m): + def subscriber(m) -> str: event.set() return "hi" pub_broker.include_router(router) - - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(pub_broker.publish("hello", queue)), + asyncio.create_task(br.publish("hello", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -651,19 +588,20 @@ def subscriber(m): assert event.is_set() subscriber.mock.assert_called_with("hello") - async def test_manual_publisher_mock( - self, router: BrokerRouter, queue: str, pub_broker: BrokerUsecase - ): + async def test_manual_publisher_mock(self, queue: str) -> None: + pub_broker = self.get_broker() + router = self.get_router() + publisher = router.publisher(queue + "resp") args, kwargs = self.get_subscriber_params(queue) @pub_broker.subscriber(*args, **kwargs) - async def m(m): + async def m(m) -> None: await publisher.publish("response") pub_broker.include_router(router) - async with pub_broker: - await pub_broker.start() - await pub_broker.publish("hello", queue) + async with self.patch_broker(pub_broker) as br: + await br.start() + await br.publish("hello", queue) publisher.mock.assert_called_with("response") diff --git a/tests/brokers/base/rpc.py b/tests/brokers/base/rpc.py deleted file mode 100644 index dcdd8e85e0..0000000000 --- a/tests/brokers/base/rpc.py +++ /dev/null @@ -1,131 +0,0 @@ -import asyncio -from abc import abstractstaticmethod -from typing import Any -from unittest.mock import MagicMock - -import anyio -import pytest - -from faststream.broker.core.usecase import BrokerUsecase -from faststream.utils.functions import timeout_scope - -from .basic import BaseTestcaseConfig - - -class BrokerRPCTestcase(BaseTestcaseConfig): - @abstractstaticmethod - def get_broker(self, apply_types: bool = False) -> BrokerUsecase[Any, Any]: - raise NotImplementedError - - def patch_broker(self, broker: BrokerUsecase[Any, Any]) -> BrokerUsecase[Any, Any]: - return broker - - @pytest.mark.asyncio - async def test_rpc(self, queue: str): - rpc_broker = self.get_broker() - - args, kwargs = self.get_subscriber_params(queue) - - @rpc_broker.subscriber(*args, **kwargs) - async def m(m): - return "Hi!" - - async with self.patch_broker(rpc_broker) as br: - await br.start() - r = await br.publish("hello", queue, rpc_timeout=3, rpc=True) - - assert r == "Hi!" - - @pytest.mark.asyncio - async def test_rpc_timeout_raises(self, queue: str): - rpc_broker = self.get_broker() - - args, kwargs = self.get_subscriber_params(queue) - - @rpc_broker.subscriber(*args, **kwargs) - async def m(m): # pragma: no cover - await anyio.sleep(1) - - async with self.patch_broker(rpc_broker) as br: - await br.start() - - with pytest.raises(TimeoutError): # pragma: no branch - await br.publish( - "hello", - queue, - rpc=True, - rpc_timeout=0, - raise_timeout=True, - ) - - @pytest.mark.asyncio - async def test_rpc_timeout_none(self, queue: str): - rpc_broker = self.get_broker() - - args, kwargs = self.get_subscriber_params(queue) - - @rpc_broker.subscriber(*args, **kwargs) - async def m(m): # pragma: no cover - await anyio.sleep(1) - - async with self.patch_broker(rpc_broker) as br: - await br.start() - - r = await br.publish( - "hello", - queue, - rpc=True, - rpc_timeout=0, - ) - - assert r is None - - @pytest.mark.asyncio - async def test_rpc_with_reply( - self, - queue: str, - mock: MagicMock, - event: asyncio.Event, - ): - rpc_broker = self.get_broker() - - reply_queue = queue + "1" - - args, kwargs = self.get_subscriber_params(reply_queue) - - @rpc_broker.subscriber(*args, **kwargs) - async def response_hanler(m: str): - mock(m) - event.set() - - args2, kwargs2 = self.get_subscriber_params(queue) - - @rpc_broker.subscriber(*args2, **kwargs2) - async def m(m): - return "1" - - async with self.patch_broker(rpc_broker) as br: - await br.start() - - await br.publish("hello", queue, reply_to=reply_queue) - - with timeout_scope(3, True): - await event.wait() - - mock.assert_called_with("1") - - -class ReplyAndConsumeForbidden: - @pytest.mark.asyncio - async def test_rpc_with_reply_and_callback(self): - rpc_broker = self.get_broker() - - async with rpc_broker: - with pytest.raises(ValueError): # noqa: PT011 - await rpc_broker.publish( - "hello", - "some", - reply_to="some", - rpc=True, - rpc_timeout=0, - ) diff --git a/tests/brokers/base/testclient.py b/tests/brokers/base/testclient.py index 08d0726f9a..6ec4c17c3a 100644 --- a/tests/brokers/base/testclient.py +++ b/tests/brokers/base/testclient.py @@ -5,42 +5,50 @@ import anyio import pytest -from faststream.testing.broker import TestBroker -from faststream.types import AnyCallable -from tests.brokers.base.consume import BrokerConsumeTestcase -from tests.brokers.base.publish import BrokerPublishTestcase -from tests.brokers.base.rpc import BrokerRPCTestcase +from .consume import BrokerConsumeTestcase +from .publish import BrokerPublishTestcase -class BrokerTestclientTestcase( - BrokerPublishTestcase, - BrokerConsumeTestcase, - BrokerRPCTestcase, -): - build_message: AnyCallable - test_class: TestBroker - +class BrokerTestclientTestcase(BrokerPublishTestcase, BrokerConsumeTestcase): @abstractmethod def get_fake_producer_class(self) -> type: raise NotImplementedError - @pytest.mark.asyncio - async def test_subscriber_mock(self, queue: str): + @pytest.mark.asyncio() + async def test_correct_clean_fake_subscribers(self) -> None: + broker = self.get_broker() + + @broker.subscriber("test") + async def handler1(msg) -> None: ... + + broker.publisher("test2") + broker.publisher("test") + + assert len(broker._subscribers) == 1 + + test_client = self.patch_broker(broker) + async with test_client as br: + assert len(br._subscribers) == 2 + + assert len(broker._subscribers) == 1 + + @pytest.mark.asyncio() + async def test_subscriber_mock(self, queue: str) -> None: test_broker = self.get_broker() args, kwargs = self.get_subscriber_params(queue) @test_broker.subscriber(*args, **kwargs) - async def m(msg): + async def m(msg) -> None: pass - async with self.test_class(test_broker): - await test_broker.start() - await test_broker.publish("hello", queue) + async with self.patch_broker(test_broker) as br: + await br.start() + await br.publish("hello", queue) m.mock.assert_called_once_with("hello") - @pytest.mark.asyncio - async def test_publisher_mock(self, queue: str): + @pytest.mark.asyncio() + async def test_publisher_mock(self, queue: str) -> None: test_broker = self.get_broker() publisher = test_broker.publisher(queue + "resp") @@ -49,16 +57,16 @@ async def test_publisher_mock(self, queue: str): @publisher @test_broker.subscriber(*args, **kwargs) - async def m(msg): + async def m(msg) -> str: return "response" - async with self.test_class(test_broker): - await test_broker.start() - await test_broker.publish("hello", queue) + async with self.patch_broker(test_broker) as br: + await br.start() + await br.publish("hello", queue) publisher.mock.assert_called_with("response") - @pytest.mark.asyncio - async def test_publisher_with_subscriber__mock(self, queue: str): + @pytest.mark.asyncio() + async def test_publisher_with_subscriber__mock(self, queue: str) -> None: test_broker = self.get_broker() publisher = test_broker.publisher(queue + "resp") @@ -67,25 +75,25 @@ async def test_publisher_with_subscriber__mock(self, queue: str): @publisher @test_broker.subscriber(*args, **kwargs) - async def m(msg): + async def m(msg) -> str: return "response" args2, kwargs2 = self.get_subscriber_params(queue + "resp") @test_broker.subscriber(*args2, **kwargs2) - async def handler_response(msg): ... + async def handler_response(msg) -> None: ... - async with self.test_class(test_broker): - await test_broker.start() + async with self.patch_broker(test_broker) as br: + await br.start() - assert len(test_broker._subscribers) == 2 + assert len(br._subscribers) == 2 - await test_broker.publish("hello", queue) + await br.publish("hello", queue) publisher.mock.assert_called_with("response") handler_response.mock.assert_called_once_with("response") - @pytest.mark.asyncio - async def test_manual_publisher_mock(self, queue: str): + @pytest.mark.asyncio() + async def test_manual_publisher_mock(self, queue: str) -> None: test_broker = self.get_broker() publisher = test_broker.publisher(queue + "resp") @@ -93,52 +101,72 @@ async def test_manual_publisher_mock(self, queue: str): args, kwargs = self.get_subscriber_params(queue) @test_broker.subscriber(*args, **kwargs) - async def m(msg): + async def m(msg) -> None: await publisher.publish("response") - async with self.test_class(test_broker): - await test_broker.start() - await test_broker.publish("hello", queue) + async with self.patch_broker(test_broker) as br: + await br.start() + await br.publish("hello", queue) publisher.mock.assert_called_with("response") - @pytest.mark.asyncio - async def test_exception_raises(self, queue: str): + @pytest.mark.asyncio() + async def test_exception_raises(self, queue: str) -> None: test_broker = self.get_broker() args, kwargs = self.get_subscriber_params(queue) @test_broker.subscriber(*args, **kwargs) async def m(msg): # pragma: no cover - raise ValueError() + raise ValueError - async with self.test_class(test_broker): - await test_broker.start() + async with self.patch_broker(test_broker) as br: + await br.start() with pytest.raises(ValueError): # noqa: PT011 - await test_broker.publish("hello", queue) + await br.publish("hello", queue) - async def test_broker_gets_patched_attrs_within_cm(self): + @pytest.mark.asyncio() + async def test_parser_exception_raises(self, queue: str) -> None: + test_broker = self.get_broker() + + def parser(msg): + raise ValueError + + args, kwargs = self.get_subscriber_params(queue, parser=parser) + + @test_broker.subscriber(*args, **kwargs) + async def m(msg): # pragma: no cover + pass + + async with self.patch_broker(test_broker) as br: + await br.start() + + with pytest.raises(ValueError): # noqa: PT011 + await br.publish("hello", queue) + + async def test_broker_gets_patched_attrs_within_cm(self, fake_producer_cls) -> None: test_broker = self.get_broker() - fake_producer_class = self.get_fake_producer_class() await test_broker.start() - async with self.test_class(test_broker) as br: + old_producer = test_broker._producer + + async with self.patch_broker(test_broker) as br: assert isinstance(br.start, Mock) assert isinstance(br._connect, Mock) assert isinstance(br.close, Mock) - assert isinstance(br._producer, fake_producer_class) + assert isinstance(br._producer, fake_producer_cls) assert not isinstance(br.start, Mock) assert not isinstance(br._connect, Mock) assert not isinstance(br.close, Mock) assert br._connection is not None - assert not isinstance(br._producer, fake_producer_class) + assert br._producer == old_producer - async def test_broker_with_real_doesnt_get_patched(self): + async def test_broker_with_real_doesnt_get_patched(self) -> None: test_broker = self.get_broker() await test_broker.start() - async with self.test_class(test_broker, with_real=True) as br: + async with self.patch_broker(test_broker, with_real=True) as br: assert not isinstance(br.start, Mock) assert not isinstance(br._connect, Mock) assert not isinstance(br.close, Mock) @@ -146,8 +174,9 @@ async def test_broker_with_real_doesnt_get_patched(self): assert br._producer is not None async def test_broker_with_real_patches_publishers_and_subscribers( - self, queue: str - ): + self, + queue: str, + ) -> None: test_broker = self.get_broker() publisher = test_broker.publisher(f"{queue}1") @@ -155,10 +184,10 @@ async def test_broker_with_real_patches_publishers_and_subscribers( args, kwargs = self.get_subscriber_params(queue) @test_broker.subscriber(*args, **kwargs) - async def m(msg): + async def m(msg) -> None: await publisher.publish(f"response: {msg}") - async with self.test_class(test_broker, with_real=True) as br: + async with self.patch_broker(test_broker, with_real=True) as br: await br.publish("hello", queue) await m.wait_call(self.timeout) m.mock.assert_called_once_with("hello") diff --git a/tests/brokers/confluent/basic.py b/tests/brokers/confluent/basic.py index 6fffc1c976..4b9e626695 100644 --- a/tests/brokers/confluent/basic.py +++ b/tests/brokers/confluent/basic.py @@ -1,17 +1,24 @@ -from typing import Any, Dict, Tuple +from typing import Any -from faststream.confluent import TopicPartition -from tests.brokers.base.basic import BaseTestcaseConfig as _Base +from faststream.confluent import ( + KafkaBroker, + KafkaRouter, + TestKafkaBroker, + TopicPartition, +) +from tests.brokers.base.basic import BaseTestcaseConfig -class ConfluentTestcaseConfig(_Base): +class ConfluentTestcaseConfig(BaseTestcaseConfig): timeout: float = 10.0 def get_subscriber_params( - self, *topics: Any, **kwargs: Any - ) -> Tuple[ - Tuple[Any, ...], - Dict[str, Any], + self, + *topics: Any, + **kwargs: Any, + ) -> tuple[ + tuple[Any, ...], + dict[str, Any], ]: if len(topics) == 1: partitions = [TopicPartition(topics[0], partition=0, offset=0)] @@ -25,3 +32,21 @@ def get_subscriber_params( "partitions": partitions, **kwargs, } + + def get_broker( + self, + apply_types: bool = False, + **kwargs: Any, + ) -> KafkaBroker: + return KafkaBroker(apply_types=apply_types, **kwargs) + + def patch_broker(self, broker: KafkaBroker, **kwargs: Any) -> KafkaBroker: + return broker + + def get_router(self, **kwargs: Any) -> KafkaRouter: + return KafkaRouter(**kwargs) + + +class ConfluentMemoryTestcaseConfig(ConfluentTestcaseConfig): + def patch_broker(self, broker: KafkaBroker, **kwargs: Any) -> KafkaBroker: + return TestKafkaBroker(broker, **kwargs) diff --git a/tests/brokers/confluent/conftest.py b/tests/brokers/confluent/conftest.py index 291ca36f09..44e82178c6 100644 --- a/tests/brokers/confluent/conftest.py +++ b/tests/brokers/confluent/conftest.py @@ -1,16 +1,13 @@ from dataclasses import dataclass import pytest -import pytest_asyncio -from faststream.confluent import KafkaBroker, KafkaRouter, TestKafkaBroker +from faststream.confluent import KafkaRouter @dataclass class Settings: - """A class to represent the settings for the Kafka broker.""" - - url = "localhost:9092" + url: str = "localhost:9092" @pytest.fixture(scope="session") @@ -18,27 +15,6 @@ def settings(): return Settings() -@pytest.fixture +@pytest.fixture() def router(): return KafkaRouter() - - -@pytest_asyncio.fixture() -async def broker(settings): - broker = KafkaBroker(settings.url, apply_types=False) - async with broker: - yield broker - - -@pytest_asyncio.fixture() -async def full_broker(settings): - broker = KafkaBroker(settings.url) - async with broker: - yield broker - - -@pytest_asyncio.fixture() -async def test_broker(): - broker = KafkaBroker() - async with TestKafkaBroker(broker) as br: - yield br diff --git a/tests/brokers/confluent/future/test_fastapi.py b/tests/brokers/confluent/future/test_fastapi.py index ee78eb79a6..4733203370 100644 --- a/tests/brokers/confluent/future/test_fastapi.py +++ b/tests/brokers/confluent/future/test_fastapi.py @@ -1,12 +1,12 @@ import pytest +from faststream.confluent.broker.router import KafkaRouter from faststream.confluent.fastapi import KafkaRouter as StreamRouter -from faststream.confluent.router import KafkaRouter from tests.brokers.base.future.fastapi import FastapiTestCase from tests.brokers.confluent.basic import ConfluentTestcaseConfig -@pytest.mark.confluent +@pytest.mark.confluent() class TestRouter(ConfluentTestcaseConfig, FastapiTestCase): router_class = StreamRouter broker_router_class = KafkaRouter diff --git a/tests/brokers/confluent/test_config.py b/tests/brokers/confluent/test_config.py new file mode 100644 index 0000000000..f3396fdadc --- /dev/null +++ b/tests/brokers/confluent/test_config.py @@ -0,0 +1,50 @@ +from unittest.mock import MagicMock + +from faststream import AckPolicy +from faststream.confluent.subscriber.config import KafkaSubscriberConfig + + +def test_default() -> None: + config = KafkaSubscriberConfig(_outer_config=MagicMock()) + + assert config.ack_policy is AckPolicy.DO_NOTHING + assert config.ack_first + assert config.connection_data == {"enable_auto_commit": True} + + +def test_ack_first() -> None: + config = KafkaSubscriberConfig( + _outer_config=MagicMock(), _ack_policy=AckPolicy.ACK_FIRST + ) + + assert config.ack_policy is AckPolicy.DO_NOTHING + assert config.ack_first + assert config.connection_data == {"enable_auto_commit": True} + + +def test_custom_ack() -> None: + config = KafkaSubscriberConfig( + _outer_config=MagicMock(), _ack_policy=AckPolicy.REJECT_ON_ERROR + ) + + assert config.ack_policy is AckPolicy.REJECT_ON_ERROR + assert config.connection_data == {} + + +def test_no_ack() -> None: + config = KafkaSubscriberConfig( + _outer_config=MagicMock(), _no_ack=True, _ack_policy=AckPolicy.ACK_FIRST + ) + + assert config.ack_policy is AckPolicy.DO_NOTHING + assert config.connection_data == {} + + +def test_auto_commit() -> None: + config = KafkaSubscriberConfig( + _outer_config=MagicMock(), _auto_commit=True, _ack_policy=AckPolicy.ACK_FIRST + ) + + assert config.ack_policy is AckPolicy.DO_NOTHING + assert config.ack_first + assert config.connection_data == {"enable_auto_commit": True} diff --git a/tests/brokers/confluent/test_connect.py b/tests/brokers/confluent/test_connect.py index 1daf8f5fcf..c8e2aeff4a 100644 --- a/tests/brokers/confluent/test_connect.py +++ b/tests/brokers/confluent/test_connect.py @@ -1,12 +1,15 @@ import pytest from dirty_equals import IsPartialDict -from faststream.confluent import KafkaBroker, config +from faststream.confluent import KafkaBroker +from faststream.confluent.helpers import config from tests.brokers.base.connection import BrokerConnectionTestcase +from .conftest import Settings -@pytest.mark.confluent -@pytest.mark.asyncio + +@pytest.mark.confluent() +@pytest.mark.asyncio() async def test_correct_config_merging(queue: str) -> None: broker = KafkaBroker( connections_max_idle_ms=1000, @@ -23,25 +26,23 @@ async def handler() -> None: ... async with broker: await broker.start() - expected_config = IsPartialDict( - { - "connections.max.idle.ms": 1000, - "compression.codec": config.CompressionCodec.lz4, - "message.max.bytes": 1000, - "debug": config.Debug.broker, - } - ) + expected_config = IsPartialDict({ + "connections.max.idle.ms": 1000, + "compression.codec": config.CompressionCodec.lz4, + "message.max.bytes": 1000, + "debug": config.Debug.broker, + }) - producer_config = broker._producer._producer.config + producer_config = broker._producer._producer.producer.config assert producer_config == expected_config - subscriber_config = next(iter(broker._subscribers.values())).consumer.config + subscriber_config = broker._subscribers[0].consumer.config assert subscriber_config == expected_config -def test_correct_config_with_dict(): +def test_correct_config_with_dict() -> None: broker = KafkaBroker( config={ "compression.codec": config.CompressionCodec.none, @@ -55,10 +56,10 @@ def test_correct_config_with_dict(): "builtin.features": config.BuiltinFeatures.gzip, "debug": config.Debug.broker, "group.protocol": config.GroupProtocol.classic, - } + }, ) - assert broker.config.as_config_dict() == { + assert broker.config.connection_config.consumer_config == IsPartialDict({ "compression.codec": config.CompressionCodec.none.value, "compression.type": config.CompressionType.none.value, "client.dns.lookup": config.ClientDNSLookup.use_all_dns_ips.value, @@ -70,12 +71,12 @@ def test_correct_config_with_dict(): "builtin.features": config.BuiltinFeatures.gzip.value, "debug": config.Debug.broker.value, "group.protocol": config.GroupProtocol.classic.value, - } + }) -@pytest.mark.confluent +@pytest.mark.confluent() class TestConnection(BrokerConnectionTestcase): broker = KafkaBroker - def get_broker_args(self, settings): + def get_broker_args(self, settings: Settings) -> dict[str, str]: return {"bootstrap_servers": settings.url} diff --git a/tests/brokers/confluent/test_consume.py b/tests/brokers/confluent/test_consume.py index 7dff51fbcb..15660fff98 100644 --- a/tests/brokers/confluent/test_consume.py +++ b/tests/brokers/confluent/test_consume.py @@ -1,11 +1,12 @@ import asyncio +from typing import Any from unittest.mock import MagicMock, patch import pytest -from faststream.confluent import KafkaBroker +from faststream import AckPolicy from faststream.confluent.annotations import KafkaMessage -from faststream.confluent.client import AsyncConfluentConsumer +from faststream.confluent.helpers.client import AsyncConfluentConsumer from faststream.exceptions import AckMessage from tests.brokers.base.consume import BrokerRealConsumeTestcase from tests.tools import spy_decorator @@ -13,15 +14,12 @@ from .basic import ConfluentTestcaseConfig -@pytest.mark.confluent +@pytest.mark.confluent() class TestConsume(ConfluentTestcaseConfig, BrokerRealConsumeTestcase): """A class to represent a test Kafka broker.""" - def get_broker(self, apply_types: bool = False): - return KafkaBroker(apply_types=apply_types) - - @pytest.mark.asyncio - async def test_consume_batch(self, queue: str): + @pytest.mark.asyncio() + async def test_consume_batch(self, queue: str) -> None: consume_broker = self.get_broker() msgs_queue = asyncio.Queue(maxsize=1) @@ -29,7 +27,7 @@ async def test_consume_batch(self, queue: str): args, kwargs = self.get_subscriber_params(queue, batch=True) @consume_broker.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg) -> None: await msgs_queue.put(msg) async with self.patch_broker(consume_broker) as br: @@ -44,25 +42,26 @@ async def handler(msg): assert [{1, "hi"}] == [set(r.result()) for r in result] - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_batch_headers( self, - mock, - event: asyncio.Event, + mock: MagicMock, queue: str, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue, batch=True) @consume_broker.subscriber(*args, **kwargs) - def subscriber(m, msg: KafkaMessage): + def subscriber(m, msg: KafkaMessage) -> None: check = all( ( msg.headers, [msg.headers] == msg.batch_headers, msg.headers.get("custom") == "1", - ) + ), ) mock(check) event.set() @@ -81,21 +80,24 @@ def subscriber(m, msg: KafkaMessage): assert event.is_set() mock.assert_called_once_with(True) - @pytest.mark.asyncio - @pytest.mark.slow - async def test_consume_ack( + @pytest.mark.asyncio() + @pytest.mark.slow() + async def test_consume_auto_ack( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params( - queue, group_id="test", auto_commit=False + queue, + group_id="test", + ack_policy=AckPolicy.REJECT_ON_ERROR, ) @consume_broker.subscriber(*args, **kwargs) - async def handler(msg: KafkaMessage): + async def handler(msg: KafkaMessage) -> None: event.set() async with self.patch_broker(consume_broker) as br: @@ -112,7 +114,7 @@ async def handler(msg: KafkaMessage): br.publish( "hello", queue, - ) + ), ), asyncio.create_task(event.wait()), ), @@ -122,21 +124,24 @@ async def handler(msg: KafkaMessage): assert event.is_set() - @pytest.mark.asyncio - @pytest.mark.slow + @pytest.mark.asyncio() + @pytest.mark.slow() async def test_consume_ack_manual( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params( - queue, group_id="test", auto_commit=False + queue, + group_id="test", + ack_policy=AckPolicy.REJECT_ON_ERROR, ) @consume_broker.subscriber(*args, **kwargs) - async def handler(msg: KafkaMessage): + async def handler(msg: KafkaMessage) -> None: await msg.ack() event.set() @@ -159,23 +164,26 @@ async def handler(msg: KafkaMessage): assert event.is_set() - @pytest.mark.asyncio - @pytest.mark.slow + @pytest.mark.asyncio() + @pytest.mark.slow() async def test_consume_ack_raise( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params( - queue, group_id="test", auto_commit=False + queue, + group_id="test", + ack_policy=AckPolicy.REJECT_ON_ERROR, ) @consume_broker.subscriber(*args, **kwargs) async def handler(msg: KafkaMessage): event.set() - raise AckMessage() + raise AckMessage async with self.patch_broker(consume_broker) as br: await br.start() @@ -196,21 +204,24 @@ async def handler(msg: KafkaMessage): assert event.is_set() - @pytest.mark.asyncio - @pytest.mark.slow + @pytest.mark.asyncio() + @pytest.mark.slow() async def test_nack( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params( - queue, group_id="test", auto_commit=False + queue, + group_id="test", + ack_policy=AckPolicy.REJECT_ON_ERROR, ) @consume_broker.subscriber(*args, **kwargs) - async def handler(msg: KafkaMessage): + async def handler(msg: KafkaMessage) -> None: await msg.nack() event.set() @@ -233,19 +244,22 @@ async def handler(msg: KafkaMessage): assert event.is_set() - @pytest.mark.asyncio - @pytest.mark.slow + @pytest.mark.asyncio() + @pytest.mark.slow() async def test_consume_no_ack( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - args, kwargs = self.get_subscriber_params(queue, group_id="test", no_ack=True) + args, kwargs = self.get_subscriber_params( + queue, group_id="test", ack_policy=AckPolicy.DO_NOTHING + ) @consume_broker.subscriber(*args, **kwargs) - async def handler(msg: KafkaMessage): + async def handler(msg: KafkaMessage) -> None: event.set() async with self.patch_broker(consume_broker) as br: @@ -262,7 +276,7 @@ async def handler(msg: KafkaMessage): br.publish( "hello", queue, - ) + ), ), asyncio.create_task(event.wait()), ), @@ -272,21 +286,24 @@ async def handler(msg: KafkaMessage): assert event.is_set() - @pytest.mark.asyncio - @pytest.mark.slow + @pytest.mark.asyncio() + @pytest.mark.slow() async def test_consume_with_no_auto_commit( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params( - queue, auto_commit=False, group_id="test" + queue, + group_id="test", + ack_policy=AckPolicy.REJECT_ON_ERROR, ) @consume_broker.subscriber(*args, **kwargs) - async def subscriber_no_auto_commit(msg: KafkaMessage): + async def subscriber_no_auto_commit(msg: KafkaMessage) -> None: await msg.nack() event.set() @@ -294,11 +311,13 @@ async def subscriber_no_auto_commit(msg: KafkaMessage): event2 = asyncio.Event() args, kwargs = self.get_subscriber_params( - queue, auto_commit=True, group_id="test" + queue, + group_id="test", + ack_policy=AckPolicy.REJECT_ON_ERROR, ) @broker2.subscriber(*args, **kwargs) - async def subscriber_with_auto_commit(m): + async def subscriber_with_auto_commit(m) -> None: event2.set() async with self.patch_broker(consume_broker) as br: @@ -323,9 +342,10 @@ async def subscriber_with_auto_commit(m): assert event.is_set() assert event2.is_set() - @pytest.mark.asyncio - @pytest.mark.slow - async def test_concurrent_consume(self, queue: str, mock: MagicMock): + @pytest.mark.asyncio() + @pytest.mark.slow() + @pytest.mark.flaky(retries=3, retry_delay=1) + async def test_concurrent_consume(self, queue: str, mock: MagicMock) -> None: event = asyncio.Event() event2 = asyncio.Event() @@ -334,7 +354,7 @@ async def test_concurrent_consume(self, queue: str, mock: MagicMock): args, kwargs = self.get_subscriber_params(queue, max_workers=2) @consume_broker.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg: Any) -> None: mock() if event.is_set(): event2.set() @@ -345,6 +365,8 @@ async def handler(msg): async with self.patch_broker(consume_broker) as br: await br.start() + await asyncio.sleep(1) # TODO: wait until subscriber is ready + for i in range(5): await br.publish(i, queue) @@ -353,9 +375,7 @@ async def handler(msg): asyncio.create_task(event.wait()), asyncio.create_task(event2.wait()), ), - timeout=3, + timeout=self.timeout, ) - assert event.is_set() - assert event2.is_set() assert mock.call_count == 2, mock.call_count diff --git a/tests/brokers/confluent/test_fastapi.py b/tests/brokers/confluent/test_fastapi.py index 0de5bb7311..a92d403b86 100644 --- a/tests/brokers/confluent/test_fastapi.py +++ b/tests/brokers/confluent/test_fastapi.py @@ -1,42 +1,41 @@ import asyncio -from typing import List -from unittest.mock import Mock +from unittest.mock import MagicMock import pytest from faststream.confluent import KafkaRouter from faststream.confluent.fastapi import KafkaRouter as StreamRouter -from faststream.confluent.testing import TestKafkaBroker, build_message from tests.brokers.base.fastapi import FastAPILocalTestcase, FastAPITestcase -from .basic import ConfluentTestcaseConfig +from .basic import ConfluentMemoryTestcaseConfig, ConfluentTestcaseConfig -@pytest.mark.confluent +@pytest.mark.confluent() class TestConfluentRouter(ConfluentTestcaseConfig, FastAPITestcase): router_class = StreamRouter broker_router_class = KafkaRouter async def test_batch_real( self, - mock: Mock, + mock: MagicMock, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() args, kwargs = self.get_subscriber_params(queue, batch=True) @router.subscriber(*args, **kwargs) - async def hello(msg: List[str]): + async def hello(msg: list[str]): event.set() return mock(msg) - async with router.broker: - await router.broker.start() + async with self.patch_broker(router.broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task(router.broker.publish("hi", queue)), + asyncio.create_task(br.publish("hi", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, @@ -46,31 +45,30 @@ async def hello(msg: List[str]): mock.assert_called_with(["hi"]) -class TestRouterLocal(ConfluentTestcaseConfig, FastAPILocalTestcase): +class TestRouterLocal(ConfluentMemoryTestcaseConfig, FastAPILocalTestcase): router_class = StreamRouter broker_router_class = KafkaRouter - broker_test = staticmethod(TestKafkaBroker) - build_message = staticmethod(build_message) async def test_batch_testclient( self, - mock: Mock, + mock: MagicMock, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() args, kwargs = self.get_subscriber_params(queue, batch=True) @router.subscriber(*args, **kwargs) - async def hello(msg: List[str]): + async def hello(msg: list[str]): event.set() return mock(msg) - async with TestKafkaBroker(router.broker): + async with self.patch_broker(router.broker) as br: await asyncio.wait( ( - asyncio.create_task(router.broker.publish("hi", queue)), + asyncio.create_task(br.publish("hi", queue)), asyncio.create_task(event.wait()), ), timeout=self.timeout, diff --git a/tests/brokers/confluent/test_include_router.py b/tests/brokers/confluent/test_include_router.py new file mode 100644 index 0000000000..c2d027fbf1 --- /dev/null +++ b/tests/brokers/confluent/test_include_router.py @@ -0,0 +1,14 @@ +from tests.brokers.base.include_router import ( + IncludePublisherTestcase, + IncludeSubscriberTestcase, +) + +from .basic import ConfluentTestcaseConfig + + +class TestSubscriber(ConfluentTestcaseConfig, IncludeSubscriberTestcase): + pass + + +class TestPublisher(ConfluentTestcaseConfig, IncludePublisherTestcase): + pass diff --git a/tests/brokers/confluent/test_logger.py b/tests/brokers/confluent/test_logger.py index 3c2e3a132a..ce1c8f145f 100644 --- a/tests/brokers/confluent/test_logger.py +++ b/tests/brokers/confluent/test_logger.py @@ -1,56 +1,28 @@ -import asyncio import logging -from typing import Any import pytest -from faststream.broker.core.usecase import BrokerUsecase -from faststream.confluent import KafkaBroker - from .basic import ConfluentTestcaseConfig -@pytest.mark.confluent +@pytest.mark.confluent() class TestLogger(ConfluentTestcaseConfig): """A class to represent a test Kafka broker.""" - def get_broker(self, apply_types: bool = False): - return KafkaBroker(apply_types=apply_types) - - def patch_broker(self, broker: BrokerUsecase[Any, Any]) -> BrokerUsecase[Any, Any]: - return broker - - @pytest.mark.asyncio - async def test_custom_logger( - self, - queue: str, - event: asyncio.Event, - ): + @pytest.mark.asyncio() + async def test_custom_logger(self, queue: str) -> None: test_logger = logging.getLogger("test_logger") - consume_broker = KafkaBroker(logger=test_logger) + broker = self.get_broker(logger=test_logger) args, kwargs = self.get_subscriber_params(queue) - @consume_broker.subscriber(*args, **kwargs) - def subscriber(m): - event.set() - - async with self.patch_broker(consume_broker) as br: - await br.start() - - for sub in br._subscribers.values(): - consumer_logger = sub.consumer.logger - assert consumer_logger == test_logger + @broker.subscriber(*args, **kwargs) + def subscriber(m) -> None: ... - producer_logger = br._producer._producer.logger - assert producer_logger == test_logger + await broker.start() - await asyncio.wait( - ( - asyncio.create_task(br.publish("hello", queue)), - asyncio.create_task(event.wait()), - ), - timeout=10, - ) + for sub in broker._subscribers: + consumer_logger = sub.consumer.logger_state.logger.logger + assert consumer_logger == test_logger - assert event.is_set() + await broker.close() diff --git a/tests/brokers/confluent/test_middlewares.py b/tests/brokers/confluent/test_middlewares.py index 17e203879c..b81c9a4325 100644 --- a/tests/brokers/confluent/test_middlewares.py +++ b/tests/brokers/confluent/test_middlewares.py @@ -1,27 +1,23 @@ import pytest -from faststream.confluent import KafkaBroker, TestKafkaBroker from tests.brokers.base.middlewares import ( ExceptionMiddlewareTestcase, MiddlewareTestcase, MiddlewaresOrderTestcase, ) -from .basic import ConfluentTestcaseConfig +from .basic import ConfluentMemoryTestcaseConfig, ConfluentTestcaseConfig -@pytest.mark.confluent -class TestMiddlewares(ConfluentTestcaseConfig, MiddlewareTestcase): - broker_class = KafkaBroker - +class TestMiddlewaresOrder(ConfluentMemoryTestcaseConfig, MiddlewaresOrderTestcase): + pass -@pytest.mark.confluent -class TestExceptionMiddlewares(ConfluentTestcaseConfig, ExceptionMiddlewareTestcase): - broker_class = KafkaBroker +@pytest.mark.confluent() +class TestMiddlewares(ConfluentTestcaseConfig, MiddlewareTestcase): + pass -class TestMiddlewaresOrder(MiddlewaresOrderTestcase): - broker_class = KafkaBroker - def patch_broker(self, broker: KafkaBroker) -> TestKafkaBroker: - return TestKafkaBroker(broker) +@pytest.mark.confluent() +class TestExceptionMiddlewares(ConfluentTestcaseConfig, ExceptionMiddlewareTestcase): + pass diff --git a/tests/brokers/confluent/test_misconfigure.py b/tests/brokers/confluent/test_misconfigure.py index 66bec159d0..2714c1b0c9 100644 --- a/tests/brokers/confluent/test_misconfigure.py +++ b/tests/brokers/confluent/test_misconfigure.py @@ -1,19 +1,89 @@ import pytest -from faststream.confluent import KafkaBroker -from faststream.confluent.router import KafkaRouter +from faststream import AckPolicy +from faststream.confluent import KafkaBroker, TopicPartition +from faststream.confluent.broker.router import KafkaRouter +from faststream.confluent.subscriber.usecase import ConcurrentDefaultSubscriber from faststream.exceptions import SetupError from faststream.nats import NatsRouter -from faststream.redis import RedisRouter def test_max_workers_with_manual(queue: str) -> None: broker = KafkaBroker() - with pytest.raises(SetupError): + with pytest.warns(DeprecationWarning): + sub = broker.subscriber(queue, max_workers=3, auto_commit=True) + assert isinstance(sub, ConcurrentDefaultSubscriber) + + with pytest.raises(SetupError), pytest.warns(DeprecationWarning): broker.subscriber(queue, max_workers=3, auto_commit=False) +def test_max_workers_with_ack_policy(queue: str) -> None: + broker = KafkaBroker() + + sub = broker.subscriber(queue, max_workers=3, ack_policy=AckPolicy.ACK_FIRST) + assert isinstance(sub, ConcurrentDefaultSubscriber) + + with pytest.raises(SetupError): + broker.subscriber(queue, max_workers=3, ack_policy=AckPolicy.REJECT_ON_ERROR) + + +def test_deprecated_options(queue: str) -> None: + broker = KafkaBroker() + + with pytest.warns(DeprecationWarning): + broker.subscriber(queue, group_id="test", auto_commit=False) + + with pytest.warns(DeprecationWarning): + broker.subscriber(queue, auto_commit=True) + + with pytest.warns(DeprecationWarning): + broker.subscriber(queue, group_id="test", no_ack=False) + + with pytest.warns(DeprecationWarning): + broker.subscriber(queue, group_id="test", no_ack=True) + + +def test_deprecated_conflicts_actual(queue: str) -> None: + broker = KafkaBroker() + + with pytest.raises(SetupError), pytest.warns(DeprecationWarning): + broker.subscriber(queue, auto_commit=False, ack_policy=AckPolicy.ACK) + + with pytest.raises(SetupError), pytest.warns(DeprecationWarning): + broker.subscriber(queue, no_ack=False, ack_policy=AckPolicy.ACK) + + +def test_manual_ack_policy_without_group(queue: str) -> None: + broker = KafkaBroker() + + broker.subscriber(queue, group_id="test", ack_policy=AckPolicy.DO_NOTHING) + + with pytest.raises(SetupError): + broker.subscriber(queue, ack_policy=AckPolicy.DO_NOTHING) + + +def test_manual_commit_without_group(queue: str) -> None: + broker = KafkaBroker() + + with pytest.warns(DeprecationWarning): + broker.subscriber(queue, group_id="test", auto_commit=False) + + with pytest.raises(SetupError), pytest.warns(DeprecationWarning): + broker.subscriber(queue, auto_commit=False) + + +def test_wrong_destination(queue: str) -> None: + broker = KafkaBroker() + + with pytest.raises(SetupError): + broker.subscriber() + + with pytest.raises(SetupError): + broker.subscriber(queue, partitions=[TopicPartition(queue, 1)]) + + def test_use_only_confluent_router() -> None: broker = KafkaBroker() router = NatsRouter() @@ -21,7 +91,7 @@ def test_use_only_confluent_router() -> None: with pytest.raises(SetupError): broker.include_router(router) - routers = [KafkaRouter(), NatsRouter(), RedisRouter()] + routers = [KafkaRouter(), NatsRouter()] with pytest.raises(SetupError): broker.include_routers(routers) diff --git a/tests/brokers/confluent/test_parser.py b/tests/brokers/confluent/test_parser.py index 36a407e100..65aa2bff15 100644 --- a/tests/brokers/confluent/test_parser.py +++ b/tests/brokers/confluent/test_parser.py @@ -1,11 +1,10 @@ import pytest -from faststream.confluent import KafkaBroker from tests.brokers.base.parser import CustomParserTestcase from .basic import ConfluentTestcaseConfig -@pytest.mark.confluent +@pytest.mark.confluent() class TestCustomParser(ConfluentTestcaseConfig, CustomParserTestcase): - broker_class = KafkaBroker + pass diff --git a/tests/brokers/confluent/test_publish.py b/tests/brokers/confluent/test_publish.py index c337953397..11d5cdc299 100644 --- a/tests/brokers/confluent/test_publish.py +++ b/tests/brokers/confluent/test_publish.py @@ -1,22 +1,19 @@ import asyncio -from unittest.mock import Mock +from unittest.mock import MagicMock import pytest from faststream import Context -from faststream.confluent import KafkaBroker, KafkaResponse +from faststream.confluent import KafkaResponse from tests.brokers.base.publish import BrokerPublishTestcase from .basic import ConfluentTestcaseConfig -@pytest.mark.confluent +@pytest.mark.confluent() class TestPublish(ConfluentTestcaseConfig, BrokerPublishTestcase): - def get_broker(self, apply_types: bool = False): - return KafkaBroker(apply_types=apply_types) - - @pytest.mark.asyncio - async def test_publish_batch(self, queue: str): + @pytest.mark.asyncio() + async def test_publish_batch(self, queue: str) -> None: pub_broker = self.get_broker() msgs_queue = asyncio.Queue(maxsize=2) @@ -24,7 +21,7 @@ async def test_publish_batch(self, queue: str): args, kwargs = self.get_subscriber_params(queue) @pub_broker.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg) -> None: await msgs_queue.put(msg) async with self.patch_broker(pub_broker) as br: @@ -42,8 +39,8 @@ async def handler(msg): assert {1, "hi"} == {r.result() for r in result} - @pytest.mark.asyncio - async def test_batch_publisher_manual(self, queue: str): + @pytest.mark.asyncio() + async def test_batch_publisher_manual(self, queue: str) -> None: pub_broker = self.get_broker() msgs_queue = asyncio.Queue(maxsize=2) @@ -51,7 +48,7 @@ async def test_batch_publisher_manual(self, queue: str): args, kwargs = self.get_subscriber_params(queue) @pub_broker.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg) -> None: await msgs_queue.put(msg) publisher = pub_broker.publisher(queue, batch=True) @@ -71,8 +68,8 @@ async def handler(msg): assert {1, "hi"} == {r.result() for r in result} - @pytest.mark.asyncio - async def test_batch_publisher_decorator(self, queue: str): + @pytest.mark.asyncio() + async def test_batch_publisher_decorator(self, queue: str) -> None: pub_broker = self.get_broker() msgs_queue = asyncio.Queue(maxsize=2) @@ -80,7 +77,7 @@ async def test_batch_publisher_decorator(self, queue: str): args, kwargs = self.get_subscriber_params(queue) @pub_broker.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg) -> None: await msgs_queue.put(msg) args2, kwargs2 = self.get_subscriber_params(queue + "1") @@ -105,13 +102,14 @@ async def pub(m): assert {1, "hi"} == {r.result() for r in result} - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_response( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) args, kwargs = self.get_subscriber_params(queue) @@ -124,7 +122,7 @@ async def handle(): args2, kwargs2 = self.get_subscriber_params(queue + "1") @pub_broker.subscriber(*args2, **kwargs2) - async def handle_next(msg=Context("message")): + async def handle_next(msg=Context("message")) -> None: mock(body=msg.body) event.set() diff --git a/tests/brokers/confluent/test_publish_command.py b/tests/brokers/confluent/test_publish_command.py new file mode 100644 index 0000000000..cb53928c71 --- /dev/null +++ b/tests/brokers/confluent/test_publish_command.py @@ -0,0 +1,14 @@ +from faststream.confluent.response import KafkaPublishCommand, KafkaResponse +from faststream.response import ensure_response +from tests.brokers.base.publish_command import BatchPublishCommandTestcase + + +class TestPublishCommand(BatchPublishCommandTestcase): + publish_command_cls = KafkaPublishCommand + + def test_kafka_response_class(self) -> None: + response = ensure_response(KafkaResponse(body=1, headers={"1": 1}, key=b"1")) + cmd = self.publish_command_cls.from_cmd(response.as_publish_command()) + assert cmd.body == 1 + assert cmd.headers == {"1": 1} + assert cmd.key == b"1" diff --git a/tests/brokers/confluent/test_requests.py b/tests/brokers/confluent/test_requests.py index 39f4677113..fac1b09331 100644 --- a/tests/brokers/confluent/test_requests.py +++ b/tests/brokers/confluent/test_requests.py @@ -1,31 +1,23 @@ +from typing import Any + import pytest from faststream import BaseMiddleware -from faststream.confluent import KafkaBroker, KafkaRouter, TestKafkaBroker from tests.brokers.base.requests import RequestsTestcase -from .basic import ConfluentTestcaseConfig +from .basic import ConfluentMemoryTestcaseConfig class Mid(BaseMiddleware): async def on_receive(self) -> None: - self.msg._raw_msg = self.msg._raw_msg * 2 + self.msg._raw_msg *= 2 async def consume_scope(self, call_next, msg): - msg._decoded_body = msg._decoded_body * 2 + msg.body *= 2 return await call_next(msg) -@pytest.mark.asyncio -class TestRequestTestClient(ConfluentTestcaseConfig, RequestsTestcase): - def get_middleware(self, **kwargs): +@pytest.mark.asyncio() +class TestRequestTestClient(ConfluentMemoryTestcaseConfig, RequestsTestcase): + def get_middleware(self, **kwargs: Any): return Mid - - def get_broker(self, **kwargs): - return KafkaBroker(**kwargs) - - def get_router(self, **kwargs): - return KafkaRouter(**kwargs) - - def patch_broker(self, broker, **kwargs): - return TestKafkaBroker(broker, **kwargs) diff --git a/tests/brokers/confluent/test_router.py b/tests/brokers/confluent/test_router.py index 746857d9de..c26198d8d4 100644 --- a/tests/brokers/confluent/test_router.py +++ b/tests/brokers/confluent/test_router.py @@ -1,19 +1,20 @@ import pytest -from faststream.confluent import KafkaPublisher, KafkaRoute, KafkaRouter +from faststream.confluent import ( + KafkaPublisher, + KafkaRoute, +) from tests.brokers.base.router import RouterLocalTestcase, RouterTestcase -from .basic import ConfluentTestcaseConfig +from .basic import ConfluentMemoryTestcaseConfig, ConfluentTestcaseConfig -@pytest.mark.confluent +@pytest.mark.confluent() class TestRouter(ConfluentTestcaseConfig, RouterTestcase): - broker_class = KafkaRouter route_class = KafkaRoute publisher_class = KafkaPublisher -class TestRouterLocal(ConfluentTestcaseConfig, RouterLocalTestcase): - broker_class = KafkaRouter +class TestRouterLocal(ConfluentMemoryTestcaseConfig, RouterLocalTestcase): route_class = KafkaRoute publisher_class = KafkaPublisher diff --git a/tests/brokers/confluent/test_security.py b/tests/brokers/confluent/test_security.py index 489c95fbe2..9664728334 100644 --- a/tests/brokers/confluent/test_security.py +++ b/tests/brokers/confluent/test_security.py @@ -1,5 +1,4 @@ from contextlib import contextmanager -from typing import Tuple from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -8,12 +7,12 @@ @contextmanager -def patch_aio_consumer_and_producer() -> Tuple[MagicMock, MagicMock]: +def patch_aio_consumer_and_producer() -> tuple[MagicMock, MagicMock]: try: producer = MagicMock(return_value=AsyncMock()) with patch( - "faststream.confluent.broker.broker.AsyncConfluentProducer", + "faststream.confluent.helpers.client.AsyncConfluentProducer", new=producer, ): yield producer @@ -21,9 +20,8 @@ def patch_aio_consumer_and_producer() -> Tuple[MagicMock, MagicMock]: pass -@pytest.mark.asyncio -@pytest.mark.confluent -async def test_base_security_pass_ssl_context(): +@pytest.mark.asyncio() +async def test_base_security_pass_ssl_context() -> None: import ssl from faststream.confluent import KafkaBroker @@ -32,15 +30,8 @@ async def test_base_security_pass_ssl_context(): ssl_context = ssl.create_default_context() security = BaseSecurity(ssl_context=ssl_context) - basic_broker = KafkaBroker("localhost:9092", security=security) - - with patch_aio_consumer_and_producer(), pytest.raises( - SetupError, match="not supported" - ) as e: - async with basic_broker: - pass - - assert ( - str(e.value) - == "ssl_context in not supported by confluent-kafka-python, please use config instead." - ) + with pytest.raises( + SetupError, + match=r"ssl_context is not supported by confluent-kafka-python, please use config instead.", + ): + KafkaBroker("localhost:9092", security=security) diff --git a/tests/brokers/confluent/test_test_client.py b/tests/brokers/confluent/test_test_client.py index fa739c2d2c..59c08afeea 100644 --- a/tests/brokers/confluent/test_test_client.py +++ b/tests/brokers/confluent/test_test_client.py @@ -3,90 +3,59 @@ import pytest -from faststream import BaseMiddleware -from faststream.confluent import KafkaBroker, TestKafkaBroker +from faststream import AckPolicy, BaseMiddleware from faststream.confluent.annotations import KafkaMessage from faststream.confluent.message import FAKE_CONSUMER from faststream.confluent.testing import FakeProducer from tests.brokers.base.testclient import BrokerTestclientTestcase from tests.tools import spy_decorator -from .basic import ConfluentTestcaseConfig +from .basic import ConfluentMemoryTestcaseConfig -@pytest.mark.asyncio -class TestTestclient(ConfluentTestcaseConfig, BrokerTestclientTestcase): - """A class to represent a test Kafka broker.""" - - test_class = TestKafkaBroker - - def get_broker(self, apply_types: bool = False): - return KafkaBroker(apply_types=apply_types) - - def patch_broker(self, broker: KafkaBroker) -> TestKafkaBroker: - return TestKafkaBroker(broker) - - def get_fake_producer_class(self) -> type: - return FakeProducer - +@pytest.mark.asyncio() +class TestTestclient(ConfluentMemoryTestcaseConfig, BrokerTestclientTestcase): async def test_message_nack_seek( self, queue: str, - ): + ) -> None: broker = self.get_broker(apply_types=True) @broker.subscriber( queue, group_id=f"{queue}-consume", - auto_commit=False, auto_offset_reset="earliest", + ack_policy=AckPolicy.REJECT_ON_ERROR, ) - async def m(msg: KafkaMessage): + async def m(msg: KafkaMessage) -> None: await msg.nack() async with self.patch_broker(broker) as br: with patch.object( - FAKE_CONSUMER, "seek", spy_decorator(FAKE_CONSUMER.seek) + FAKE_CONSUMER, + "seek", + spy_decorator(FAKE_CONSUMER.seek), ) as mocked: await br.publish("hello", queue) m.mock.assert_called_once_with("hello") mocked.mock.assert_called_once() - async def test_publisher_autoflush_mock( - self, - queue: str, - ): - broker = self.get_broker() - - publisher = broker.publisher(queue + "1", autoflush=True) - publisher.flush = AsyncMock() - - @publisher - @broker.subscriber(queue) - async def m(msg): - pass - - async with TestKafkaBroker(broker) as br: - await br.publish("hello", queue) - publisher.flush.assert_awaited_once() - m.mock.assert_called_once_with("hello") - publisher.mock.assert_called_once() - - @pytest.mark.confluent + @pytest.mark.confluent() async def test_with_real_testclient( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + broker = self.get_broker() args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - def subscriber(m): + def subscriber(m) -> None: event.set() - async with TestKafkaBroker(broker, with_real=True) as br: + async with self.patch_broker(broker, with_real=True) as br: await asyncio.wait( ( asyncio.create_task(br.publish("hello", queue)), @@ -97,41 +66,52 @@ def subscriber(m): assert event.is_set() - @pytest.mark.confluent - async def test_autoflush_with_real_testclient( - self, - queue: str, - event: asyncio.Event, - ): + async def test_publisher_autoflush_mock(self, queue: str) -> None: broker = self.get_broker() - publisher = broker.publisher(queue, autoflush=True) - args, kwargs = self.get_subscriber_params(queue) + publisher = broker.publisher(queue + "1", autoflush=True) + publisher.flush = AsyncMock() @publisher - @broker.subscriber(*args, **kwargs) - def subscriber(m): - event.set() + @broker.subscriber(queue) + async def m(msg): + pass - async with TestKafkaBroker(broker, with_real=True) as br: - await asyncio.wait( - ( - asyncio.create_task(br.publish("hello", queue)), - asyncio.create_task(event.wait()), - ), - timeout=10, - ) + async with self.patch_broker(broker) as br: + await br.publish("hello", queue) - assert event.is_set() + m.mock.assert_called_once_with("hello") + publisher.mock.assert_called_once() + + publisher.flush.assert_awaited_once() + + async def test_batch_publisher_autoflush_mock(self, queue: str) -> None: + broker = self.get_broker() + + publisher = broker.publisher(queue + "1", batch=True, autoflush=True) + publisher.flush = AsyncMock() + + @publisher + @broker.subscriber(queue) + async def m(msg): + return 1, 2, 3 + + async with self.patch_broker(broker) as br: + await br.publish("hello", queue) + + m.mock.assert_called_once_with("hello") + publisher.mock.assert_called_once_with([1, 2, 3]) + + publisher.flush.assert_awaited_once() async def test_batch_pub_by_default_pub( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(queue, batch=True) - async def m(msg): + async def m(msg) -> None: pass async with self.patch_broker(broker) as br: @@ -141,11 +121,11 @@ async def m(msg): async def test_batch_pub_by_pub_batch( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(queue, batch=True) - async def m(msg): + async def m(msg) -> None: pass async with self.patch_broker(broker) as br: @@ -155,7 +135,7 @@ async def m(msg): async def test_batch_publisher_mock( self, queue: str, - ): + ) -> None: broker = self.get_broker() publisher = broker.publisher(queue + "1", batch=True) @@ -170,26 +150,6 @@ async def m(msg): m.mock.assert_called_once_with("hello") publisher.mock.assert_called_once_with([1, 2, 3]) - async def test_batch_publisher_autoflush_mock( - self, - queue: str, - ): - broker = self.get_broker() - - publisher = broker.publisher(queue + "1", batch=True, autoflush=True) - publisher.flush = AsyncMock() - - @publisher - @broker.subscriber(queue) - async def m(msg): - return 1, 2, 3 - - async with TestKafkaBroker(broker) as br: - await br.publish("hello", queue) - publisher.flush.assert_awaited_once() - m.mock.assert_called_once_with("hello") - publisher.mock.assert_called_once_with([1, 2, 3]) - async def test_respect_middleware(self, queue): routes = [] @@ -198,22 +158,22 @@ async def on_receive(self) -> None: routes.append(None) return await super().on_receive() - broker = KafkaBroker(middlewares=(Middleware,)) + broker = self.get_broker(middlewares=(Middleware,)) @broker.subscriber(queue) - async def h1(): ... + async def h1(msg) -> None: ... @broker.subscriber(queue + "1") - async def h2(): ... + async def h2(msg) -> None: ... - async with TestKafkaBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("", queue) await br.publish("", queue + "1") assert len(routes) == 2 - @pytest.mark.confluent - async def test_real_respect_middleware(self, queue): + @pytest.mark.confluent() + async def test_real_respect_middleware(self, queue) -> None: routes = [] class Middleware(BaseMiddleware): @@ -221,19 +181,19 @@ async def on_receive(self) -> None: routes.append(None) return await super().on_receive() - broker = KafkaBroker(middlewares=(Middleware,)) + broker = self.get_broker(middlewares=(Middleware,)) args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def h1(): ... + async def h1(msg) -> None: ... args2, kwargs2 = self.get_subscriber_params(queue + "1") @broker.subscriber(*args2, **kwargs2) - async def h2(): ... + async def h2(msg) -> None: ... - async with TestKafkaBroker(broker, with_real=True) as br: + async with self.patch_broker(broker, with_real=True) as br: await br.publish("", queue) await br.publish("", queue + "1") await h1.wait_call(10) @@ -244,81 +204,90 @@ async def h2(): ... async def test_multiple_subscribers_different_groups( self, queue: str, - test_broker: KafkaBroker, - ): - @test_broker.subscriber(queue, group_id="group1") - async def subscriber1(): ... + ) -> None: + broker = self.get_broker() + + @broker.subscriber(queue, group_id="group1") + async def subscriber1(msg) -> None: ... - @test_broker.subscriber(queue, group_id="group2") - async def subscriber2(): ... + @broker.subscriber(queue, group_id="group2") + async def subscriber2(msg) -> None: ... - await test_broker.start() - await test_broker.publish("", queue) + async with self.patch_broker(broker) as br: + await br.start() + await br.publish("", queue) - assert subscriber1.mock.call_count == 1 - assert subscriber2.mock.call_count == 1 + assert subscriber1.mock.call_count == 1 + assert subscriber2.mock.call_count == 1 async def test_multiple_subscribers_same_group( self, queue: str, - test_broker: KafkaBroker, - ): - @test_broker.subscriber(queue, group_id="group1") - async def subscriber1(): ... + ) -> None: + broker = self.get_broker() - @test_broker.subscriber(queue, group_id="group1") - async def subscriber2(): ... + @broker.subscriber(queue, group_id="group1") + async def subscriber1(msg) -> None: ... - await test_broker.start() - await test_broker.publish("", queue) + @broker.subscriber(queue, group_id="group1") + async def subscriber2(msg) -> None: ... - assert subscriber1.mock.call_count == 1 - assert subscriber2.mock.call_count == 0 + async with self.patch_broker(broker) as br: + await br.start() + await br.publish("", queue) + + assert subscriber1.mock.call_count == 1 + assert subscriber2.mock.call_count == 0 async def test_multiple_batch_subscriber_with_different_group( self, - test_broker: KafkaBroker, queue: str, - ): - @test_broker.subscriber(queue, batch=True, group_id="group1") - async def subscriber1(): ... + ) -> None: + broker = self.get_broker() - @test_broker.subscriber(queue, batch=True, group_id="group2") - async def subscriber2(): ... + @broker.subscriber(queue, batch=True, group_id="group1") + async def subscriber1(msg) -> None: ... - await test_broker.start() - await test_broker.publish("", queue) + @broker.subscriber(queue, batch=True, group_id="group2") + async def subscriber2(msg) -> None: ... + + async with self.patch_broker(broker) as br: + await br.start() + await br.publish("", queue) - assert subscriber1.mock.call_count == 1 - assert subscriber2.mock.call_count == 1 + assert subscriber1.mock.call_count == 1 + assert subscriber2.mock.call_count == 1 async def test_multiple_batch_subscriber_with_same_group( self, - test_broker: KafkaBroker, queue: str, - ): - @test_broker.subscriber(queue, batch=True, group_id="group1") - async def subscriber1(): ... + ) -> None: + broker = self.get_broker() + + @broker.subscriber(queue, batch=True, group_id="group1") + async def subscriber1(msg) -> None: ... - @test_broker.subscriber(queue, batch=True, group_id="group1") - async def subscriber2(): ... + @broker.subscriber(queue, batch=True, group_id="group1") + async def subscriber2(msg) -> None: ... - await test_broker.start() - await test_broker.publish("", queue) + async with self.patch_broker(broker) as br: + await br.start() + await br.publish("", queue) - assert subscriber1.mock.call_count == 1 - assert subscriber2.mock.call_count == 0 + assert subscriber1.mock.call_count == 1 + assert subscriber2.mock.call_count == 0 - @pytest.mark.confluent - async def test_broker_gets_patched_attrs_within_cm(self): - await super().test_broker_gets_patched_attrs_within_cm() + @pytest.mark.confluent() + async def test_broker_gets_patched_attrs_within_cm(self) -> None: + await super().test_broker_gets_patched_attrs_within_cm(FakeProducer) - @pytest.mark.confluent - async def test_broker_with_real_doesnt_get_patched(self): + @pytest.mark.confluent() + async def test_broker_with_real_doesnt_get_patched(self) -> None: await super().test_broker_with_real_doesnt_get_patched() - @pytest.mark.confluent + @pytest.mark.confluent() async def test_broker_with_real_patches_publishers_and_subscribers( - self, queue: str - ): + self, + queue: str, + ) -> None: await super().test_broker_with_real_patches_publishers_and_subscribers(queue) diff --git a/tests/brokers/confluent/test_test_reentrancy.py b/tests/brokers/confluent/test_test_reentrancy.py index 8edcc441aa..9a8bf9b0a7 100644 --- a/tests/brokers/confluent/test_test_reentrancy.py +++ b/tests/brokers/confluent/test_test_reentrancy.py @@ -15,20 +15,22 @@ @to_output_data @broker.subscriber( - partitions=[TopicPartition(first_topic_name, 0)], auto_offset_reset="earliest" + partitions=[TopicPartition(first_topic_name, 0)], + auto_offset_reset="earliest", ) async def on_input_data(msg: int): return msg + 1 @broker.subscriber( - partitions=[TopicPartition(out_topic_name, 0)], auto_offset_reset="earliest" + partitions=[TopicPartition(out_topic_name, 0)], + auto_offset_reset="earliest", ) -async def on_output_data(msg: int): +async def on_output_data(msg: int) -> None: pass -async def _test_with_broker(with_real: bool): +async def _test_with_broker(with_real: bool) -> None: async with TestKafkaBroker(broker, with_real=with_real) as tester: await tester.publish(1, first_topic_name) @@ -39,22 +41,23 @@ async def _test_with_broker(with_real: bool): on_output_data.mock.assert_called_once_with(2) -@pytest.mark.asyncio -async def test_with_fake_broker(): +@pytest.mark.asyncio() +async def test_with_fake_broker() -> None: await _test_with_broker(False) await _test_with_broker(False) -@pytest.mark.asyncio -@pytest.mark.confluent -async def test_with_real_broker(): +@pytest.mark.asyncio() +@pytest.mark.confluent() +@pytest.mark.flaky(retries=3, only_on=[AssertionError], retry_delay=1) +async def test_with_real_broker() -> None: await _test_with_broker(True) await _test_with_broker(True) -async def _test_with_temp_subscriber(): +async def _test_with_temp_subscriber() -> None: @broker.subscriber("output_data", auto_offset_reset="earliest") - async def on_output_data(msg: int): + async def on_output_data(msg: int) -> None: pass async with TestKafkaBroker(broker) as tester: @@ -67,13 +70,13 @@ async def on_output_data(msg: int): on_output_data.mock.assert_called_once_with(2) -@pytest.mark.asyncio +@pytest.mark.asyncio() @pytest.mark.skip( reason=( "Failed due `on_output_data` subscriber creates inside test and doesn't removed after " "https://github.com/ag2ai/faststream/issues/556" ) ) -async def test_with_temp_subscriber(): +async def test_with_temp_subscriber() -> None: await _test_with_temp_subscriber() await _test_with_temp_subscriber() diff --git a/tests/brokers/kafka/basic.py b/tests/brokers/kafka/basic.py new file mode 100644 index 0000000000..39c095a637 --- /dev/null +++ b/tests/brokers/kafka/basic.py @@ -0,0 +1,24 @@ +from typing import Any + +from faststream.kafka import KafkaBroker, KafkaRouter, TestKafkaBroker +from tests.brokers.base.basic import BaseTestcaseConfig + + +class KafkaTestcaseConfig(BaseTestcaseConfig): + def get_broker( + self, + apply_types: bool = False, + **kwargs: Any, + ) -> KafkaBroker: + return KafkaBroker(apply_types=apply_types, **kwargs) + + def patch_broker(self, broker: KafkaBroker, **kwargs: Any) -> KafkaBroker: + return broker + + def get_router(self, **kwargs: Any) -> KafkaRouter: + return KafkaRouter(**kwargs) + + +class KafkaMemoryTestcaseConfig(KafkaTestcaseConfig): + def patch_broker(self, broker: KafkaBroker, **kwargs: Any) -> KafkaBroker: + return TestKafkaBroker(broker, **kwargs) diff --git a/tests/brokers/kafka/conftest.py b/tests/brokers/kafka/conftest.py index c20d0f9f28..1eede17ed7 100644 --- a/tests/brokers/kafka/conftest.py +++ b/tests/brokers/kafka/conftest.py @@ -1,42 +1,20 @@ from dataclasses import dataclass import pytest -import pytest_asyncio -from faststream.kafka import KafkaBroker, KafkaRouter, TestKafkaBroker +from faststream.kafka import KafkaRouter @dataclass class Settings: - url = "localhost:9092" + url: str = "localhost:9092" @pytest.fixture(scope="session") -def settings(): +def settings() -> Settings: return Settings() -@pytest.fixture -def router(): +@pytest.fixture() +def router() -> KafkaRouter: return KafkaRouter() - - -@pytest_asyncio.fixture() -async def broker(settings): - broker = KafkaBroker(settings.url, apply_types=False) - async with broker: - yield broker - - -@pytest_asyncio.fixture() -async def full_broker(settings): - broker = KafkaBroker(settings.url) - async with broker: - yield broker - - -@pytest_asyncio.fixture() -async def test_broker(): - broker = KafkaBroker() - async with TestKafkaBroker(broker) as br: - yield br diff --git a/tests/brokers/kafka/future/test_fastapi.py b/tests/brokers/kafka/future/test_fastapi.py index 6660547442..945cb24e49 100644 --- a/tests/brokers/kafka/future/test_fastapi.py +++ b/tests/brokers/kafka/future/test_fastapi.py @@ -1,11 +1,11 @@ import pytest +from faststream.kafka.broker import KafkaRouter from faststream.kafka.fastapi import KafkaRouter as StreamRouter -from faststream.kafka.router import KafkaRouter from tests.brokers.base.future.fastapi import FastapiTestCase -@pytest.mark.kafka +@pytest.mark.kafka() class TestRouter(FastapiTestCase): router_class = StreamRouter broker_router_class = KafkaRouter diff --git a/tests/brokers/kafka/test_config.py b/tests/brokers/kafka/test_config.py new file mode 100644 index 0000000000..1007aa475f --- /dev/null +++ b/tests/brokers/kafka/test_config.py @@ -0,0 +1,40 @@ +from faststream import AckPolicy +from faststream.kafka.subscriber.config import KafkaSubscriberConfig + + +def test_default() -> None: + config = KafkaSubscriberConfig() + + assert config.ack_policy is AckPolicy.DO_NOTHING + assert config.ack_first + assert config.connection_args == {"enable_auto_commit": True} + + +def test_ack_first() -> None: + config = KafkaSubscriberConfig(_ack_policy=AckPolicy.ACK_FIRST) + + assert config.ack_policy is AckPolicy.DO_NOTHING + assert config.ack_first + assert config.connection_args == {"enable_auto_commit": True} + + +def test_custom_ack() -> None: + config = KafkaSubscriberConfig(_ack_policy=AckPolicy.REJECT_ON_ERROR) + + assert config.ack_policy is AckPolicy.REJECT_ON_ERROR + assert config.connection_args == {} + + +def test_no_ack() -> None: + config = KafkaSubscriberConfig(_no_ack=True, _ack_policy=AckPolicy.ACK_FIRST) + + assert config.ack_policy is AckPolicy.DO_NOTHING + assert config.connection_args == {} + + +def test_auto_commit() -> None: + config = KafkaSubscriberConfig(_auto_commit=True, _ack_policy=AckPolicy.ACK_FIRST) + + assert config.ack_policy is AckPolicy.DO_NOTHING + assert config.ack_first + assert config.connection_args == {"enable_auto_commit": True} diff --git a/tests/brokers/kafka/test_connect.py b/tests/brokers/kafka/test_connect.py index 8feb6dd4e7..b13f4792ad 100644 --- a/tests/brokers/kafka/test_connect.py +++ b/tests/brokers/kafka/test_connect.py @@ -1,12 +1,16 @@ +from typing import Any + import pytest from faststream.kafka import KafkaBroker from tests.brokers.base.connection import BrokerConnectionTestcase +from .conftest import Settings + -@pytest.mark.kafka +@pytest.mark.kafka() class TestConnection(BrokerConnectionTestcase): broker = KafkaBroker - def get_broker_args(self, settings): + def get_broker_args(self, settings: Settings) -> dict[str, Any]: return {"bootstrap_servers": settings.url} diff --git a/tests/brokers/kafka/test_consume.py b/tests/brokers/kafka/test_consume.py index de9951623d..ddfc4d0076 100644 --- a/tests/brokers/kafka/test_consume.py +++ b/tests/brokers/kafka/test_consume.py @@ -1,46 +1,48 @@ import asyncio import logging -from unittest.mock import MagicMock, Mock, patch +from typing import Any +from unittest.mock import MagicMock, patch import pytest from aiokafka import AIOKafkaConsumer, ConsumerRebalanceListener from aiokafka.admin import AIOKafkaAdminClient, NewTopic +from aiokafka.structs import RecordMetadata +from faststream import AckPolicy from faststream.exceptions import AckMessage -from faststream.kafka import KafkaBroker, TopicPartition -from faststream.kafka.annotations import KafkaMessage -from faststream.kafka.listener import LoggingListenerProxy +from faststream.kafka import KafkaBroker, KafkaMessage, TopicPartition +from faststream.kafka.listener import _LoggingListener from tests.brokers.base.consume import BrokerRealConsumeTestcase from tests.tools import spy_decorator +from .basic import KafkaTestcaseConfig -@pytest.mark.kafka -class TestConsume(BrokerRealConsumeTestcase): - def get_broker(self, apply_types: bool = False): - return KafkaBroker(apply_types=apply_types) - @pytest.mark.asyncio +@pytest.mark.kafka() +class TestConsume(KafkaTestcaseConfig, BrokerRealConsumeTestcase): + @pytest.mark.asyncio() async def test_consume_by_pattern( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber(queue) - async def handler(msg): + async def handler(msg) -> None: event.set() pattern_event = asyncio.Event() @consume_broker.subscriber(pattern=f"{queue[:-1]}*") - async def pattern_handler(msg): + async def pattern_handler(msg: Any) -> None: pattern_event.set() async with self.patch_broker(consume_broker) as br: await br.start() - await br.publish(1, topic=queue) + result = await br.publish(1, topic=queue) await asyncio.wait( ( @@ -50,18 +52,19 @@ async def pattern_handler(msg): ), timeout=3, ) + assert isinstance(result, RecordMetadata), result assert event.is_set() assert pattern_event.is_set() - @pytest.mark.asyncio - async def test_consume_batch(self, queue: str): + @pytest.mark.asyncio() + async def test_consume_batch(self, queue: str) -> None: consume_broker = self.get_broker() msgs_queue = asyncio.Queue(maxsize=1) @consume_broker.subscriber(queue, batch=True) - async def handler(msg): + async def handler(msg: Any) -> None: await msgs_queue.put(msg) async with self.patch_broker(consume_broker) as br: @@ -76,23 +79,24 @@ async def handler(msg): assert [{1, "hi"}] == [set(r.result()) for r in result] - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_batch_headers( self, - mock, - event: asyncio.Event, + mock: MagicMock, queue: str, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber(queue, batch=True) - def subscriber(m, msg: KafkaMessage): + def subscriber(msg: KafkaMessage) -> None: check = all( ( msg.headers, [msg.headers] == msg.batch_headers, msg.headers.get("custom") == "1", - ) + ), ) mock(check) event.set() @@ -111,24 +115,25 @@ def subscriber(m, msg: KafkaMessage): assert event.is_set() mock.assert_called_once_with(True) - @pytest.mark.asyncio - @pytest.mark.slow - async def test_consume_ack( - self, - queue: str, - event: asyncio.Event, - ): + @pytest.mark.asyncio() + @pytest.mark.slow() + @pytest.mark.flaky(retries=3, only_on=[AssertionError], retry_delay=1) + async def test_consume_auto_ack(self, event: asyncio.Event, queue: str) -> None: consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue, group_id="test", auto_commit=False) - async def handler(msg: KafkaMessage): + @consume_broker.subscriber( + queue, group_id="test", ack_policy=AckPolicy.REJECT_ON_ERROR + ) + async def handler(msg: KafkaMessage) -> None: event.set() async with self.patch_broker(consume_broker) as br: await br.start() with patch.object( - AIOKafkaConsumer, "commit", spy_decorator(AIOKafkaConsumer.commit) + AIOKafkaConsumer, + "commit", + spy_decorator(AIOKafkaConsumer.commit), ) as m: await asyncio.wait( ( @@ -136,7 +141,7 @@ async def handler(msg: KafkaMessage): consume_broker.publish( "hello", queue, - ) + ), ), asyncio.create_task(event.wait()), ), @@ -144,20 +149,19 @@ async def handler(msg: KafkaMessage): ) m.mock.assert_called_once() - assert event.is_set() - - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_manual_partition_consume( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() tp1 = TopicPartition(queue, partition=0) @consume_broker.subscriber(partitions=[tp1]) - async def handler_tp1(msg): + async def handler_tp1(msg: Any) -> None: event.set() async with self.patch_broker(consume_broker) as br: @@ -173,17 +177,20 @@ async def handler_tp1(msg): assert event.is_set() - @pytest.mark.asyncio - @pytest.mark.slow + @pytest.mark.asyncio() + @pytest.mark.slow() async def test_consume_ack_manual( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue, group_id="test", auto_commit=False) - async def handler(msg: KafkaMessage): + @consume_broker.subscriber( + queue, group_id="test", ack_policy=AckPolicy.REJECT_ON_ERROR + ) + async def handler(msg: KafkaMessage) -> None: await msg.ack() event.set() @@ -191,7 +198,9 @@ async def handler(msg: KafkaMessage): await br.start() with patch.object( - AIOKafkaConsumer, "commit", spy_decorator(AIOKafkaConsumer.commit) + AIOKafkaConsumer, + "commit", + spy_decorator(AIOKafkaConsumer.commit), ) as m: await asyncio.wait( ( @@ -199,7 +208,7 @@ async def handler(msg: KafkaMessage): br.publish( "hello", queue, - ) + ), ), asyncio.create_task(event.wait()), ), @@ -209,25 +218,30 @@ async def handler(msg: KafkaMessage): assert event.is_set() - @pytest.mark.asyncio - @pytest.mark.slow - async def test_consume_ack_raise( + @pytest.mark.asyncio() + @pytest.mark.slow() + async def test_consume_ack_by_raise( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue, group_id="test", auto_commit=False) - async def handler(msg: KafkaMessage): + @consume_broker.subscriber( + queue, group_id="test", ack_policy=AckPolicy.REJECT_ON_ERROR + ) + async def handler(msg: KafkaMessage) -> None: event.set() - raise AckMessage() + raise AckMessage async with self.patch_broker(consume_broker) as br: await br.start() with patch.object( - AIOKafkaConsumer, "commit", spy_decorator(AIOKafkaConsumer.commit) + AIOKafkaConsumer, + "commit", + spy_decorator(AIOKafkaConsumer.commit), ) as m: await asyncio.wait( ( @@ -235,7 +249,7 @@ async def handler(msg: KafkaMessage): br.publish( "hello", queue, - ) + ), ), asyncio.create_task(event.wait()), ), @@ -245,17 +259,20 @@ async def handler(msg: KafkaMessage): assert event.is_set() - @pytest.mark.asyncio - @pytest.mark.slow - async def test_nack( + @pytest.mark.asyncio() + @pytest.mark.slow() + async def test_manual_nack( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue, group_id="test", auto_commit=False) - async def handler(msg: KafkaMessage): + @consume_broker.subscriber( + queue, group_id="test", ack_policy=AckPolicy.REJECT_ON_ERROR + ) + async def handler(msg: KafkaMessage) -> None: await msg.nack() event.set() @@ -263,7 +280,9 @@ async def handler(msg: KafkaMessage): await br.start() with patch.object( - AIOKafkaConsumer, "commit", spy_decorator(AIOKafkaConsumer.commit) + AIOKafkaConsumer, + "commit", + spy_decorator(AIOKafkaConsumer.commit), ) as m: await asyncio.wait( ( @@ -271,7 +290,7 @@ async def handler(msg: KafkaMessage): br.publish( "hello", queue, - ) + ), ), asyncio.create_task(event.wait()), ), @@ -281,24 +300,29 @@ async def handler(msg: KafkaMessage): assert event.is_set() - @pytest.mark.asyncio - @pytest.mark.slow + @pytest.mark.asyncio() + @pytest.mark.slow() async def test_consume_no_ack( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue, group_id="test", no_ack=True) - async def handler(msg: KafkaMessage): + @consume_broker.subscriber( + queue, group_id="test", ack_policy=AckPolicy.DO_NOTHING + ) + async def handler(msg: KafkaMessage) -> None: event.set() async with self.patch_broker(consume_broker) as br: await br.start() with patch.object( - AIOKafkaConsumer, "commit", spy_decorator(AIOKafkaConsumer.commit) + AIOKafkaConsumer, + "commit", + spy_decorator(AIOKafkaConsumer.commit), ) as m: await asyncio.wait( ( @@ -306,7 +330,7 @@ async def handler(msg: KafkaMessage): br.publish( "hello", queue, - ) + ), ), asyncio.create_task(event.wait()), ), @@ -316,9 +340,67 @@ async def handler(msg: KafkaMessage): assert event.is_set() - @pytest.mark.asyncio - @pytest.mark.slow - async def test_concurrent_consume(self, queue: str, mock: MagicMock): + @pytest.mark.asyncio() + async def test_consume_without_value( + self, + mock: MagicMock, + queue: str, + event: asyncio.Event, + ) -> None: + consume_broker = self.get_broker() + + @consume_broker.subscriber(queue) + async def handler(msg: bytes) -> None: + event.set() + mock(msg) + + async with self.patch_broker(consume_broker) as br: + await br.start() + + await asyncio.wait( + ( + asyncio.create_task( + br._producer._producer.producer.send(queue, key=b"") + ), + asyncio.create_task(event.wait()), + ), + timeout=3, + ) + + mock.assert_called_once_with(b"") + + @pytest.mark.asyncio() + async def test_consume_batch_without_value( + self, + mock: MagicMock, + queue: str, + event: asyncio.Event, + ) -> None: + consume_broker = self.get_broker() + + @consume_broker.subscriber(queue, batch=True) + async def handler(msg: list[bytes]) -> None: + event.set() + mock(msg) + + async with self.patch_broker(consume_broker) as br: + await br.start() + + await asyncio.wait( + ( + asyncio.create_task( + br._producer._producer.producer.send(queue, key=b"") + ), + asyncio.create_task(event.wait()), + ), + timeout=3, + ) + + mock.assert_called_once_with([b""]) + + @pytest.mark.asyncio() + @pytest.mark.slow() + async def test_concurrent_consume(self, queue: str, mock: MagicMock) -> None: event = asyncio.Event() event2 = asyncio.Event() @@ -327,7 +409,7 @@ async def test_concurrent_consume(self, queue: str, mock: MagicMock): args, kwargs = self.get_subscriber_params(queue, max_workers=2) @consume_broker.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg: Any) -> None: mock() if event.is_set(): event2.set() @@ -355,81 +437,80 @@ async def handler(msg): assert event2.is_set() assert mock.call_count == 2, mock.call_count - @pytest.mark.asyncio - @pytest.mark.slow + @pytest.mark.asyncio() + @pytest.mark.slow() + @pytest.mark.flaky(retries=3, retry_delay=1) async def test_concurrent_consume_between_partitions( self, queue: str, - ): - inputs = set() + ) -> None: + await create_topic(queue, 3) + + consume_broker = self.get_broker(apply_types=True) - admin_client = AIOKafkaAdminClient() - try: - await admin_client.start() - await admin_client.create_topics([NewTopic(queue, 2, 1)]) - finally: - await admin_client.close() + event1, event2 = asyncio.Event(), asyncio.Event() - consume_broker = self.get_broker() + consumers = set() @consume_broker.subscriber( queue, max_workers=3, - auto_commit=False, + ack_policy=AckPolicy.ACK, group_id="service_1", ) - async def handler(msg: str): - nonlocal inputs - inputs.add(msg) - await asyncio.sleep(1) + async def handler(message: KafkaMessage) -> None: + nonlocal consumers + consumers.add(getattr(message.raw_message, "consumer", None)) + if event1.is_set(): + event2.set() + else: + event1.set() async with self.patch_broker(consume_broker) as broker: await broker.start() + await broker.publish("hello1", queue, partition=0) + await broker.publish("hello2", queue, partition=1) + await asyncio.wait( ( - asyncio.create_task(broker.publish("hello1", queue, partition=0)), - asyncio.create_task(broker.publish("hello3", queue, partition=0)), - asyncio.create_task(broker.publish("hello2", queue, partition=1)), - asyncio.create_task(broker.publish("hello4", queue, partition=1)), - asyncio.create_task(broker.publish("hello5", queue, partition=0)), - asyncio.create_task(asyncio.sleep(0.5)), + asyncio.create_task(event1.wait()), + asyncio.create_task(event2.wait()), ), - timeout=1, + timeout=10, ) - assert inputs == {"hello1", "hello2"} - await asyncio.sleep(1) - assert inputs == {"hello1", "hello2", "hello3", "hello4"} - await asyncio.sleep(1) - assert inputs == {"hello1", "hello2", "hello3", "hello4", "hello5"} + assert event1.is_set() + assert event2.is_set() - await broker.close() + assert len(consumers) == 2 - @pytest.mark.asyncio - @pytest.mark.slow - @pytest.mark.parametrize("with_explicit_commit", [True, False]) + @pytest.mark.asyncio() + @pytest.mark.slow() + @pytest.mark.flaky(retries=3, retry_delay=1) + @pytest.mark.parametrize( + "with_explicit_commit", + ( + pytest.param(True, id="manual commit"), + pytest.param(False, id="commit after process"), + ), + ) async def test_concurrent_consume_between_partitions_commit( self, queue: str, with_explicit_commit: bool, - ): - admin_client = AIOKafkaAdminClient() - try: - await admin_client.start() - await admin_client.create_topics([NewTopic(queue, 2, 1)]) - finally: - await admin_client.close() + ) -> None: + await create_topic(queue, 2) consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber( queue, max_workers=3, - auto_commit=False, + ack_policy=AckPolicy.ACK, group_id="service_1", ) - async def handler(msg: KafkaMessage): + async def handler(msg: KafkaMessage) -> None: await asyncio.sleep(0.7) if with_explicit_commit: await msg.ack() @@ -453,197 +534,127 @@ async def handler(msg: KafkaMessage): ), asyncio.create_task(asyncio.sleep(1)), ), - timeout=1, + timeout=10, ) assert mock.mock.call_count == 2 - await broker.close() - @pytest.mark.asyncio - @pytest.mark.slow - @pytest.mark.parametrize( - ("partitions", "warning"), - [ - pytest.param(2, True, id="unassigned consumers"), - pytest.param(3, False, id="no unassigned consumers"), - ], +@pytest.mark.asyncio() +@pytest.mark.slow() +@pytest.mark.kafka() +@pytest.mark.flaky(retries=3, retry_delay=1) +@pytest.mark.parametrize( + ("partitions", "warning"), + ( + pytest.param(2, True, id="partitions < consumers"), + pytest.param(3, False, id="partitions > consumers"), + ), +) +async def test_concurrent_consume_between_partitions_assignment_warning( + partitions: int, + warning: bool, + queue: str, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(_LoggingListener, "_log_unassigned_consumer_delay_seconds", 0) + + admin_client = AIOKafkaAdminClient() + try: + await admin_client.start() + await admin_client.create_topics([NewTopic(queue, partitions, 1)]) + finally: + await admin_client.close() + + logger = MagicMock() + consume_broker = KafkaBroker(logger=logger) + + @consume_broker.subscriber( + queue, + max_workers=3, + ack_policy=AckPolicy.NACK_ON_ERROR, + group_id="service_1", ) - async def test_concurrent_consume_between_partitions_assignment_warning( - self, - queue: str, - partitions: int, - warning: bool, - monkeypatch: pytest.MonkeyPatch, - ): - monkeypatch.setattr( - LoggingListenerProxy, "_log_unassigned_consumer_delay_seconds", 0 - ) - - admin_client = AIOKafkaAdminClient() - try: - await admin_client.start() - await admin_client.create_topics([NewTopic(queue, partitions, 1)]) - finally: - await admin_client.close() - - consume_broker = self.get_broker() - - @consume_broker.subscriber( - queue, - max_workers=3, - auto_commit=False, - group_id="service_1", - ) - async def handler(msg: str): - pass - - with patch.object(consume_broker, "logger", Mock(handlers=[])) as mock: - async with self.patch_broker(consume_broker) as broker: - await broker.start() - await broker.close() - if warning: - assert ( - len( - [ - x - for x in mock.log.call_args_list - if x[0][0] == logging.WARNING - ] - ) - == 2 - ) - else: - assert ( - len( - [ - x - for x in mock.log.call_args_list - if x[0][0] == logging.WARNING - ] - ) - == 0 - ) + async def handler(msg: str) -> None: + pass - @pytest.mark.asyncio - async def test_consume_without_value( - self, - mock: MagicMock, - queue: str, - event: asyncio.Event, - ) -> None: - consume_broker = self.get_broker() + async with consume_broker as broker: + await broker.start() - @consume_broker.subscriber(queue) - async def handler(msg): - event.set() - mock(msg) + warning_logs = [x for x in logger.log.call_args_list if x[0][0] == logging.WARNING] - async with self.patch_broker(consume_broker) as br: - await br.start() + if warning: + assert len(warning_logs) == 2 - await asyncio.wait( - ( - asyncio.create_task(br._producer._producer.send(queue, key=b"")), - asyncio.create_task(event.wait()), - ), - timeout=3, - ) + else: + assert len(warning_logs) == 0 - mock.assert_called_once_with(b"") - @pytest.mark.asyncio - async def test_consume_batch_without_value( +@pytest.mark.asyncio() +@pytest.mark.slow() +@pytest.mark.kafka() +class TestListener(KafkaTestcaseConfig): + async def test_sync_listener( self, - mock: MagicMock, queue: str, + mock: MagicMock, event: asyncio.Event, ) -> None: consume_broker = self.get_broker() - @consume_broker.subscriber(queue, batch=True) - async def handler(msg): - event.set() - mock(msg) - - async with self.patch_broker(consume_broker) as br: - await br.start() - - await asyncio.wait( - ( - asyncio.create_task(br._producer._producer.send(queue, key=b"")), - asyncio.create_task(event.wait()), - ), - timeout=3, - ) - - mock.assert_called_once_with([b""]) - - @pytest.mark.asyncio - @pytest.mark.slow - @pytest.mark.parametrize("max_workers", [1, 2]) - async def test_listener_sync(self, queue: str, max_workers: int): - called_assigned = False - called_revoked = False - - consume_broker = self.get_broker() - class CustomListener(ConsumerRebalanceListener): - def on_partitions_revoked(self, revoked): - nonlocal called_revoked - called_revoked = True + def on_partitions_revoked(self, revoked: set[str]) -> None: + mock.on_partitions_revoked() - def on_partitions_assigned(self, assigned): - nonlocal called_assigned - called_assigned = True + def on_partitions_assigned(self, assigned: set[str]) -> None: + mock.on_partitions_assigned() + event.set() - @consume_broker.subscriber( + consume_broker.subscriber( queue, - max_workers=max_workers, - auto_commit=False, + ack_policy=AckPolicy.DO_NOTHING, group_id="service_1", listener=CustomListener(), ) - async def handler(msg: str): - pass async with self.patch_broker(consume_broker) as broker: await broker.start() - await broker.close() - assert called_assigned is True - assert called_revoked is True + await asyncio.wait((asyncio.create_task(event.wait()),), timeout=3.0) - @pytest.mark.asyncio - @pytest.mark.slow - @pytest.mark.parametrize("max_workers", [1, 2]) - async def test_listener_async(self, queue: str, max_workers: int): - called_assigned = False - called_revoked = False + assert event.is_set() + mock.on_partitions_assigned.assert_called_once() + mock.on_partitions_revoked.assert_called_once() + async def test_listener_async(self, queue: str, mock: MagicMock) -> None: consume_broker = self.get_broker() class CustomListener(ConsumerRebalanceListener): - async def on_partitions_revoked(self, revoked): - nonlocal called_revoked - called_revoked = True + async def on_partitions_revoked(self, revoked: set[str]) -> None: + mock.on_partitions_revoked() - async def on_partitions_assigned(self, assigned): - nonlocal called_assigned - called_assigned = True + async def on_partitions_assigned(self, assigned: set[str]) -> None: + mock.on_partitions_assigned() - @consume_broker.subscriber( + consume_broker.subscriber( queue, - max_workers=max_workers, - auto_commit=False, + ack_policy=AckPolicy.DO_NOTHING, group_id="service_1", listener=CustomListener(), ) - async def handler(msg: str): - pass async with self.patch_broker(consume_broker) as broker: await broker.start() - await broker.close() - assert called_assigned is True - assert called_revoked is True + mock.on_partitions_assigned.assert_called_once() + mock.on_partitions_revoked.assert_called_once() + + +async def create_topic(topic: str, partitions: int) -> None: + admin_client = AIOKafkaAdminClient() + try: + await admin_client.start() + await admin_client.create_topics([ + NewTopic(topic, partitions, 1), + ]) + finally: + await admin_client.close() diff --git a/tests/brokers/kafka/test_fastapi.py b/tests/brokers/kafka/test_fastapi.py index 509466bc65..415bff66a2 100644 --- a/tests/brokers/kafka/test_fastapi.py +++ b/tests/brokers/kafka/test_fastapi.py @@ -1,30 +1,31 @@ import asyncio -from typing import List -from unittest.mock import Mock +from unittest.mock import MagicMock import pytest from faststream.kafka import KafkaRouter from faststream.kafka.fastapi import KafkaRouter as StreamRouter -from faststream.kafka.testing import TestKafkaBroker, build_message from tests.brokers.base.fastapi import FastAPILocalTestcase, FastAPITestcase +from .basic import KafkaMemoryTestcaseConfig -@pytest.mark.kafka + +@pytest.mark.kafka() class TestKafkaRouter(FastAPITestcase): router_class = StreamRouter broker_router_class = KafkaRouter async def test_batch_real( self, - mock: Mock, + mock: MagicMock, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() @router.subscriber(queue, batch=True) - async def hello(msg: List[str]): + async def hello(msg: list[str]): event.set() return mock(msg) @@ -42,29 +43,28 @@ async def hello(msg: List[str]): mock.assert_called_with(["hi"]) -class TestRouterLocal(FastAPILocalTestcase): +class TestRouterLocal(KafkaMemoryTestcaseConfig, FastAPILocalTestcase): router_class = StreamRouter broker_router_class = KafkaRouter - broker_test = staticmethod(TestKafkaBroker) - build_message = staticmethod(build_message) async def test_batch_testclient( self, - mock: Mock, + mock: MagicMock, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() @router.subscriber(queue, batch=True) - async def hello(msg: List[str]): + async def hello(msg: list[str]): event.set() return mock(msg) - async with TestKafkaBroker(router.broker): + async with self.patch_broker(router.broker) as br: await asyncio.wait( ( - asyncio.create_task(router.broker.publish("hi", queue)), + asyncio.create_task(br.publish("hi", queue)), asyncio.create_task(event.wait()), ), timeout=3, diff --git a/tests/brokers/kafka/test_include_router.py b/tests/brokers/kafka/test_include_router.py new file mode 100644 index 0000000000..9acb0f8c79 --- /dev/null +++ b/tests/brokers/kafka/test_include_router.py @@ -0,0 +1,14 @@ +from tests.brokers.base.include_router import ( + IncludePublisherTestcase, + IncludeSubscriberTestcase, +) + +from .basic import KafkaTestcaseConfig + + +class TestSubscriber(KafkaTestcaseConfig, IncludeSubscriberTestcase): + pass + + +class TestPublisher(KafkaTestcaseConfig, IncludePublisherTestcase): + pass diff --git a/tests/brokers/kafka/test_middlewares.py b/tests/brokers/kafka/test_middlewares.py index 13f7e79349..58d5bf7d31 100644 --- a/tests/brokers/kafka/test_middlewares.py +++ b/tests/brokers/kafka/test_middlewares.py @@ -1,25 +1,23 @@ import pytest -from faststream.kafka import KafkaBroker, TestKafkaBroker from tests.brokers.base.middlewares import ( ExceptionMiddlewareTestcase, MiddlewareTestcase, MiddlewaresOrderTestcase, ) +from .basic import KafkaMemoryTestcaseConfig, KafkaTestcaseConfig -@pytest.mark.kafka -class TestMiddlewares(MiddlewareTestcase): - broker_class = KafkaBroker +class TestMiddlewaresOrder(KafkaMemoryTestcaseConfig, MiddlewaresOrderTestcase): + pass -@pytest.mark.kafka -class TestExceptionMiddlewares(ExceptionMiddlewareTestcase): - broker_class = KafkaBroker +@pytest.mark.kafka() +class TestMiddlewares(KafkaTestcaseConfig, MiddlewareTestcase): + pass -class TestMiddlewaresOrder(MiddlewaresOrderTestcase): - broker_class = KafkaBroker - def patch_broker(self, broker: KafkaBroker) -> TestKafkaBroker: - return TestKafkaBroker(broker) +@pytest.mark.kafka() +class TestExceptionMiddlewares(KafkaTestcaseConfig, ExceptionMiddlewareTestcase): + pass diff --git a/tests/brokers/kafka/test_misconfigure.py b/tests/brokers/kafka/test_misconfigure.py index 75d3d2f384..0035f534ca 100644 --- a/tests/brokers/kafka/test_misconfigure.py +++ b/tests/brokers/kafka/test_misconfigure.py @@ -1,30 +1,110 @@ +from typing import Any + import pytest -from aiokafka import TopicPartition +from faststream import AckPolicy from faststream.exceptions import SetupError -from faststream.kafka import KafkaBroker, KafkaRouter +from faststream.kafka import KafkaBroker, KafkaRouter, TopicPartition +from faststream.kafka.subscriber.usecase import ( + ConcurrentBetweenPartitionsSubscriber, + ConcurrentDefaultSubscriber, +) from faststream.nats import NatsRouter from faststream.rabbit import RabbitRouter -def test_max_workers_with_manual_commit_with_multiple_queues() -> None: +@pytest.mark.parametrize( + ("args", "kwargs"), + ( + pytest.param( + (), + {}, + id="no destination", + ), + pytest.param( + ("topic",), + {"partitions": [TopicPartition("topic", 1)]}, + id="topic and partitions", + ), + pytest.param( + ("topic",), + {"pattern": ".*"}, + id="topic and pattern", + ), + pytest.param( + (), + { + "partitions": [TopicPartition("topic", 1)], + "pattern": ".*", + }, + id="partitions and pattern", + ), + pytest.param( + ("queue1", "queue2"), + {"max_workers": 3, "ack_policy": AckPolicy.ACK}, + id="multiple topics with manual commit", + ), + pytest.param( + (), + { + "pattern": "pattern", + "max_workers": 3, + "ack_policy": AckPolicy.ACK, + }, + id="pattern with manual commit", + ), + pytest.param( + (), + { + "partitions": [TopicPartition(topic="topic", partition=1)], + "max_workers": 3, + "ack_policy": AckPolicy.ACK, + }, + id="partitions with manual commit", + ), + ), +) +def test_wrong_destination(args: list[str], kwargs: dict[str, Any]) -> None: + with pytest.raises(SetupError): + KafkaBroker().subscriber(*args, **kwargs) + + +def test_deprecated_options(queue: str) -> None: broker = KafkaBroker() - with pytest.raises(SetupError): - broker.subscriber(["queue1", "queue2"], max_workers=3, auto_commit=False) + with pytest.warns(DeprecationWarning): + broker.subscriber(queue, group_id="test", auto_commit=False) + + with pytest.warns(DeprecationWarning): + broker.subscriber(queue, auto_commit=True) + + with pytest.warns(DeprecationWarning): + broker.subscriber(queue, no_ack=False) + with pytest.warns(DeprecationWarning): + broker.subscriber(queue, group_id="test", no_ack=True) -def test_max_workers_with_manual_commit_with_pattern() -> None: + +def test_deprecated_conflicts_actual(queue: str) -> None: broker = KafkaBroker() - with pytest.raises(SetupError): - broker.subscriber(pattern="pattern", max_workers=3, auto_commit=False) + with pytest.raises(SetupError), pytest.warns(DeprecationWarning): + broker.subscriber(queue, auto_commit=False, ack_policy=AckPolicy.ACK) + + with pytest.raises(SetupError), pytest.warns(DeprecationWarning): + broker.subscriber(queue, no_ack=False, ack_policy=AckPolicy.ACK) -def test_max_workers_with_manual_commit_partitions() -> None: +def test_max_workers_configuration(queue: str) -> None: broker = KafkaBroker() - with pytest.raises(SetupError): + sub = broker.subscriber(queue, max_workers=3, ack_policy=AckPolicy.ACK_FIRST) + assert isinstance(sub, ConcurrentDefaultSubscriber) + + sub = broker.subscriber(queue, max_workers=3, ack_policy=AckPolicy.REJECT_ON_ERROR) + assert isinstance(sub, ConcurrentBetweenPartitionsSubscriber) + + with pytest.raises(SetupError), pytest.warns(DeprecationWarning): broker.subscriber( partitions=[TopicPartition(topic="topic", partition=1)], max_workers=3, diff --git a/tests/brokers/kafka/test_parser.py b/tests/brokers/kafka/test_parser.py index 1725c15e6c..0e229bbd37 100644 --- a/tests/brokers/kafka/test_parser.py +++ b/tests/brokers/kafka/test_parser.py @@ -1,9 +1,10 @@ import pytest -from faststream.kafka import KafkaBroker from tests.brokers.base.parser import CustomParserTestcase +from .basic import KafkaTestcaseConfig -@pytest.mark.kafka -class TestCustomParser(CustomParserTestcase): - broker_class = KafkaBroker + +@pytest.mark.kafka() +class TestCustomParser(KafkaTestcaseConfig, CustomParserTestcase): + pass diff --git a/tests/brokers/kafka/test_publish.py b/tests/brokers/kafka/test_publish.py index ac2c866362..b07e9a73d8 100644 --- a/tests/brokers/kafka/test_publish.py +++ b/tests/brokers/kafka/test_publish.py @@ -1,35 +1,33 @@ import asyncio -from typing import Any -from unittest.mock import Mock +from unittest.mock import MagicMock import pytest +from aiokafka.structs import RecordMetadata from faststream import Context -from faststream.kafka import KafkaBroker, KafkaResponse +from faststream.kafka import KafkaResponse from faststream.kafka.exceptions import BatchBufferOverflowException from tests.brokers.base.publish import BrokerPublishTestcase +from .basic import KafkaTestcaseConfig -@pytest.mark.kafka -class TestPublish(BrokerPublishTestcase): - def get_broker(self, apply_types: bool = False, **kwargs: Any) -> KafkaBroker: - return KafkaBroker(apply_types=apply_types, **kwargs) - @pytest.mark.asyncio - async def test_publish_batch(self, queue: str): +@pytest.mark.kafka() +class TestPublish(KafkaTestcaseConfig, BrokerPublishTestcase): + @pytest.mark.asyncio() + async def test_publish_batch(self, queue: str) -> None: pub_broker = self.get_broker() msgs_queue = asyncio.Queue(maxsize=2) @pub_broker.subscriber(queue) - async def handler(msg): + async def handler(msg) -> None: await msgs_queue.put(msg) async with self.patch_broker(pub_broker) as br: await br.start() - await br.publish_batch(1, "hi", topic=queue) - + record_metadata = await br.publish_batch(1, "hi", topic=queue) result, _ = await asyncio.wait( ( asyncio.create_task(msgs_queue.get()), @@ -37,17 +35,18 @@ async def handler(msg): ), timeout=3, ) + assert isinstance(record_metadata, RecordMetadata) assert {1, "hi"} == {r.result() for r in result} - @pytest.mark.asyncio - async def test_batch_publisher_manual(self, queue: str): + @pytest.mark.asyncio() + async def test_batch_publisher_manual(self, queue: str) -> None: pub_broker = self.get_broker() msgs_queue = asyncio.Queue(maxsize=2) @pub_broker.subscriber(queue) - async def handler(msg): + async def handler(msg) -> None: await msgs_queue.put(msg) publisher = pub_broker.publisher(queue, batch=True) @@ -67,14 +66,14 @@ async def handler(msg): assert {1, "hi"} == {r.result() for r in result} - @pytest.mark.asyncio - async def test_batch_publisher_decorator(self, queue: str): + @pytest.mark.asyncio() + async def test_batch_publisher_decorator(self, queue: str) -> None: pub_broker = self.get_broker() msgs_queue = asyncio.Queue(maxsize=2) @pub_broker.subscriber(queue) - async def handler(msg): + async def handler(msg) -> None: await msgs_queue.put(msg) @pub_broker.publisher(queue, batch=True) @@ -85,7 +84,7 @@ async def pub(m): async with self.patch_broker(pub_broker) as br: await br.start() - await br.publish("", queue + "1") + record_metadata = await br.publish("", queue + "1") result, _ = await asyncio.wait( ( @@ -94,16 +93,18 @@ async def pub(m): ), timeout=3, ) + assert isinstance(record_metadata, RecordMetadata) assert {1, "hi"} == {r.result() for r in result} - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_response( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) @pub_broker.subscriber(queue) @@ -112,7 +113,7 @@ async def handle(): return KafkaResponse(1, key=b"1") @pub_broker.subscriber(queue + "1") - async def handle_next(msg=Context("message")): + async def handle_next(msg=Context("message")) -> None: mock( body=msg.body, key=msg.raw_message.key, @@ -136,9 +137,31 @@ async def handle_next(msg=Context("message")): key=b"1", ) - @pytest.mark.asyncio + @pytest.mark.asyncio() + async def test_return_future( + self, + queue: str, + mock: MagicMock, + ) -> None: + pub_broker = self.get_broker() + + @pub_broker.subscriber(queue) + async def handler(m) -> None: + pass + + async with self.patch_broker(pub_broker) as br: + await br.start() + + batch_record_metadata_future = await br.publish_batch( + 1, "hi", topic=queue, no_confirm=True + ) + record_metadata_future = await br.publish("", topic=queue, no_confirm=True) + assert isinstance(batch_record_metadata_future, asyncio.Future) + assert isinstance(record_metadata_future, asyncio.Future) + + @pytest.mark.asyncio() async def test_raise_buffer_overflow_exception( - self, queue: str, mock: Mock + self, queue: str, mock: MagicMock ) -> None: pub_broker = self.get_broker(max_batch_size=16) diff --git a/tests/brokers/kafka/test_publish_command.py b/tests/brokers/kafka/test_publish_command.py new file mode 100644 index 0000000000..fa7f9d81d3 --- /dev/null +++ b/tests/brokers/kafka/test_publish_command.py @@ -0,0 +1,14 @@ +from faststream.kafka.response import KafkaPublishCommand, KafkaResponse +from faststream.response import ensure_response +from tests.brokers.base.publish_command import BatchPublishCommandTestcase + + +class TestPublishCommand(BatchPublishCommandTestcase): + publish_command_cls = KafkaPublishCommand + + def test_kafka_response_class(self) -> None: + response = ensure_response(KafkaResponse(body=1, headers={"1": 1}, key=b"1")) + cmd = self.publish_command_cls.from_cmd(response.as_publish_command()) + assert cmd.body == 1 + assert cmd.headers == {"1": 1} + assert cmd.key == b"1" diff --git a/tests/brokers/kafka/test_requests.py b/tests/brokers/kafka/test_requests.py index a518b2fa43..c3d7a4cb4d 100644 --- a/tests/brokers/kafka/test_requests.py +++ b/tests/brokers/kafka/test_requests.py @@ -1,29 +1,23 @@ +from typing import Any + import pytest from faststream import BaseMiddleware -from faststream.kafka import KafkaBroker, KafkaRouter, TestKafkaBroker from tests.brokers.base.requests import RequestsTestcase +from .basic import KafkaMemoryTestcaseConfig + class Mid(BaseMiddleware): async def on_receive(self) -> None: - self.msg.value = self.msg.value * 2 + self.msg.value *= 2 async def consume_scope(self, call_next, msg): - msg._decoded_body = msg._decoded_body * 2 + msg.body *= 2 return await call_next(msg) -@pytest.mark.asyncio -class TestRequestTestClient(RequestsTestcase): - def get_middleware(self, **kwargs): +@pytest.mark.asyncio() +class TestRequestTestClient(KafkaMemoryTestcaseConfig, RequestsTestcase): + def get_middleware(self, **kwargs: Any): return Mid - - def get_broker(self, **kwargs): - return KafkaBroker(**kwargs) - - def get_router(self, **kwargs): - return KafkaRouter(**kwargs) - - def patch_broker(self, broker, **kwargs): - return TestKafkaBroker(broker, **kwargs) diff --git a/tests/brokers/kafka/test_router.py b/tests/brokers/kafka/test_router.py index 17ef78d942..e9b27f5a01 100644 --- a/tests/brokers/kafka/test_router.py +++ b/tests/brokers/kafka/test_router.py @@ -1,17 +1,20 @@ import pytest -from faststream.kafka import KafkaPublisher, KafkaRoute, KafkaRouter +from faststream.kafka import ( + KafkaPublisher, + KafkaRoute, +) from tests.brokers.base.router import RouterLocalTestcase, RouterTestcase +from .basic import KafkaMemoryTestcaseConfig, KafkaTestcaseConfig -@pytest.mark.kafka -class TestRouter(RouterTestcase): - broker_class = KafkaRouter + +@pytest.mark.kafka() +class TestRouter(KafkaTestcaseConfig, RouterTestcase): route_class = KafkaRoute publisher_class = KafkaPublisher -class TestRouterLocal(RouterLocalTestcase): - broker_class = KafkaRouter +class TestRouterLocal(KafkaMemoryTestcaseConfig, RouterLocalTestcase): route_class = KafkaRoute publisher_class = KafkaPublisher diff --git a/tests/brokers/kafka/test_stuff.py b/tests/brokers/kafka/test_stuff.py deleted file mode 100644 index 83c5c61d86..0000000000 --- a/tests/brokers/kafka/test_stuff.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - -from faststream.kafka import KafkaBroker - - -def test_wrong_subscriber(): - broker = KafkaBroker() - - with pytest.raises(ValueError): # noqa: PT011 - broker.subscriber("test", auto_commit=False)(lambda: None) diff --git a/tests/brokers/kafka/test_test_client.py b/tests/brokers/kafka/test_test_client.py index 41b0bc5d98..c5e0ceb3e5 100644 --- a/tests/brokers/kafka/test_test_client.py +++ b/tests/brokers/kafka/test_test_client.py @@ -3,36 +3,27 @@ import pytest -from faststream import BaseMiddleware -from faststream.kafka import KafkaBroker, TestKafkaBroker, TopicPartition +from faststream import AckPolicy, BaseMiddleware +from faststream.kafka import TopicPartition from faststream.kafka.annotations import KafkaMessage from faststream.kafka.message import FAKE_CONSUMER from faststream.kafka.testing import FakeProducer from tests.brokers.base.testclient import BrokerTestclientTestcase from tests.tools import spy_decorator +from .basic import KafkaMemoryTestcaseConfig -@pytest.mark.asyncio -class TestTestclient(BrokerTestclientTestcase): - test_class = TestKafkaBroker - - def get_broker(self, apply_types: bool = False): - return KafkaBroker(apply_types=apply_types) - - def patch_broker(self, broker: KafkaBroker) -> TestKafkaBroker: - return TestKafkaBroker(broker) - - def get_fake_producer_class(self) -> type: - return FakeProducer +@pytest.mark.asyncio() +class TestTestclient(KafkaMemoryTestcaseConfig, BrokerTestclientTestcase): async def test_partition_match( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(partitions=[TopicPartition(queue, 1)]) - async def m(msg): + async def m(msg) -> None: pass async with self.patch_broker(broker) as br: @@ -43,11 +34,11 @@ async def m(msg): async def test_partition_match_exect( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(partitions=[TopicPartition(queue, 1)]) - async def m(msg): + async def m(msg) -> None: pass async with self.patch_broker(broker) as br: @@ -58,15 +49,15 @@ async def m(msg): async def test_partition_missmatch( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(partitions=[TopicPartition(queue, 1)]) - async def m(msg): + async def m(msg) -> None: pass @broker.subscriber(queue) - async def m2(msg): + async def m2(msg) -> None: pass async with self.patch_broker(broker) as br: @@ -78,16 +69,18 @@ async def m2(msg): async def test_message_nack_seek( self, queue: str, - ): + ) -> None: broker = self.get_broker(apply_types=True) - @broker.subscriber(queue) - async def m(msg: KafkaMessage): + @broker.subscriber(queue, group_id=f"{queue}1", ack_policy=AckPolicy.DO_NOTHING) + async def m(msg: KafkaMessage) -> None: await msg.nack() async with self.patch_broker(broker) as br: with patch.object( - FAKE_CONSUMER, "seek", spy_decorator(FAKE_CONSUMER.seek) + FAKE_CONSUMER, + "seek", + spy_decorator(FAKE_CONSUMER.seek), ) as mocked: await br.publish("hello", queue) mocked.mock.assert_called_once() @@ -95,7 +88,7 @@ async def m(msg: KafkaMessage): async def test_publisher_autoflush_mock( self, queue: str, - ): + ) -> None: broker = self.get_broker() publisher = broker.publisher(queue + "1", autoflush=True) @@ -104,53 +97,52 @@ async def test_publisher_autoflush_mock( @publisher @broker.subscriber(queue) async def m(msg): - pass + return 1 - async with TestKafkaBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("hello", queue) - publisher.flush.assert_awaited_once() + m.mock.assert_called_once_with("hello") - publisher.mock.assert_called_once() + publisher.mock.assert_called_once_with(1) - @pytest.mark.kafka - async def test_with_real_testclient( + publisher.flush.assert_awaited_once() + + async def test_batch_publisher_autoflush_mock( self, queue: str, - event: asyncio.Event, - ): + ) -> None: broker = self.get_broker() + publisher = broker.publisher(queue + "1", batch=True, autoflush=True) + publisher.flush = AsyncMock() + + @publisher @broker.subscriber(queue) - def subscriber(m): - event.set() + async def m(msg): + return 1, 2, 3 - async with TestKafkaBroker(broker, with_real=True) as br: - await asyncio.wait( - ( - asyncio.create_task(br.publish("hello", queue)), - asyncio.create_task(event.wait()), - ), - timeout=3, - ) + async with self.patch_broker(broker) as br: + await br.publish("hello", queue) - assert event.is_set() + m.mock.assert_called_once_with("hello") + publisher.mock.assert_called_once_with([1, 2, 3]) - @pytest.mark.kafka - async def test_autoflush_with_real_testclient( + publisher.flush.assert_awaited_once() + + @pytest.mark.kafka() + async def test_with_real_testclient( self, queue: str, - event: asyncio.Event, - ): - broker = self.get_broker() + ) -> None: + event = asyncio.Event() - publisher = broker.publisher(queue + "1", autoflush=True) + broker = self.get_broker() - @publisher @broker.subscriber(queue) - def subscriber(m): + def subscriber(m) -> None: event.set() - async with TestKafkaBroker(broker, with_real=True) as br: + async with self.patch_broker(broker, with_real=True) as br: await asyncio.wait( ( asyncio.create_task(br.publish("hello", queue)), @@ -164,35 +156,35 @@ def subscriber(m): async def test_batch_pub_by_default_pub( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(queue, batch=True) - async def m(msg): + async def m(msg) -> None: pass - async with TestKafkaBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("hello", queue) m.mock.assert_called_once_with(["hello"]) async def test_batch_pub_by_pub_batch( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(queue, batch=True) - async def m(msg): + async def m(msg) -> None: pass - async with TestKafkaBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish_batch("hello", topic=queue) m.mock.assert_called_once_with(["hello"]) async def test_batch_publisher_mock( self, queue: str, - ): + ) -> None: broker = self.get_broker() publisher = broker.publisher(queue + "1", batch=True) @@ -202,32 +194,12 @@ async def test_batch_publisher_mock( async def m(msg): return 1, 2, 3 - async with TestKafkaBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("hello", queue) m.mock.assert_called_once_with("hello") publisher.mock.assert_called_once_with([1, 2, 3]) - async def test_batch_publisher_autoflush_mock( - self, - queue: str, - ): - broker = self.get_broker() - - publisher = broker.publisher(queue + "1", batch=True, autoflush=True) - publisher.flush = AsyncMock() - - @publisher - @broker.subscriber(queue) - async def m(msg): - return 1, 2, 3 - - async with TestKafkaBroker(broker) as br: - await br.publish("hello", queue) - publisher.flush.assert_awaited_once() - m.mock.assert_called_once_with("hello") - publisher.mock.assert_called_once_with([1, 2, 3]) - - async def test_respect_middleware(self, queue): + async def test_respect_middleware(self, queue: str) -> None: routes = [] class Middleware(BaseMiddleware): @@ -235,22 +207,22 @@ async def on_receive(self) -> None: routes.append(None) return await super().on_receive() - broker = KafkaBroker(middlewares=(Middleware,)) + broker = self.get_broker(middlewares=(Middleware,)) @broker.subscriber(queue) - async def h1(): ... + async def h1(msg) -> None: ... @broker.subscriber(queue + "1") - async def h2(): ... + async def h2(msg) -> None: ... - async with TestKafkaBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("", queue) await br.publish("", queue + "1") assert len(routes) == 2 - @pytest.mark.kafka - async def test_real_respect_middleware(self, queue): + @pytest.mark.kafka() + async def test_real_respect_middleware(self, queue: str) -> None: routes = [] class Middleware(BaseMiddleware): @@ -258,15 +230,15 @@ async def on_receive(self) -> None: routes.append(None) return await super().on_receive() - broker = KafkaBroker(middlewares=(Middleware,)) + broker = self.get_broker(middlewares=(Middleware,)) @broker.subscriber(queue) - async def h1(): ... + async def h1(msg) -> None: ... @broker.subscriber(queue + "1") - async def h2(): ... + async def h2(msg) -> None: ... - async with TestKafkaBroker(broker, with_real=True) as br: + async with self.patch_broker(broker, with_real=True) as br: await br.publish("", queue) await br.publish("", queue + "1") await h1.wait_call(3) @@ -277,81 +249,90 @@ async def h2(): ... async def test_multiple_subscribers_different_groups( self, queue: str, - test_broker: KafkaBroker, - ): + ) -> None: + test_broker = self.get_broker() + @test_broker.subscriber(queue, group_id="group1") - async def subscriber1(): ... + async def subscriber1(msg) -> None: ... @test_broker.subscriber(queue, group_id="group2") - async def subscriber2(): ... + async def subscriber2(msg) -> None: ... - await test_broker.start() - await test_broker.publish("", queue) + async with self.patch_broker(test_broker) as br: + await br.start() + await br.publish("", queue) - assert subscriber1.mock.call_count == 1 - assert subscriber2.mock.call_count == 1 + assert subscriber1.mock.call_count == 1 + assert subscriber2.mock.call_count == 1 async def test_multiple_subscribers_same_group( self, queue: str, - test_broker: KafkaBroker, - ): - @test_broker.subscriber(queue, group_id="group1") - async def subscriber1(): ... + ) -> None: + broker = self.get_broker() - @test_broker.subscriber(queue, group_id="group1") - async def subscriber2(): ... + @broker.subscriber(queue, group_id="group1") + async def subscriber1(msg) -> None: ... + + @broker.subscriber(queue, group_id="group1") + async def subscriber2(msg) -> None: ... - await test_broker.start() - await test_broker.publish("", queue) + async with self.patch_broker(broker) as br: + await br.start() + await br.publish("", queue) - assert subscriber1.mock.call_count == 1 - assert subscriber2.mock.call_count == 0 + assert subscriber1.mock.call_count == 1 + assert subscriber2.mock.call_count == 0 async def test_multiple_batch_subscriber_with_different_group( self, queue: str, - test_broker: KafkaBroker, - ): - @test_broker.subscriber(queue, batch=True, group_id="group1") - async def subscriber1(): ... + ) -> None: + broker = self.get_broker() - @test_broker.subscriber(queue, batch=True, group_id="group2") - async def subscriber2(): ... + @broker.subscriber(queue, batch=True, group_id="group1") + async def subscriber1(msg) -> None: ... - await test_broker.start() - await test_broker.publish("", queue) + @broker.subscriber(queue, batch=True, group_id="group2") + async def subscriber2(msg) -> None: ... - assert subscriber1.mock.call_count == 1 - assert subscriber2.mock.call_count == 1 + async with self.patch_broker(broker) as br: + await br.start() + await br.publish("", queue) + + assert subscriber1.mock.call_count == 1 + assert subscriber2.mock.call_count == 1 async def test_multiple_batch_subscriber_with_same_group( self, queue: str, - test_broker: KafkaBroker, - ): - @test_broker.subscriber(queue, batch=True, group_id="group1") - async def subscriber1(): ... + ) -> None: + broker = self.get_broker() - @test_broker.subscriber(queue, batch=True, group_id="group1") - async def subscriber2(): ... + @broker.subscriber(queue, batch=True, group_id="group1") + async def subscriber1(msg) -> None: ... - await test_broker.start() - await test_broker.publish("", queue) + @broker.subscriber(queue, batch=True, group_id="group1") + async def subscriber2(msg) -> None: ... - assert subscriber1.mock.call_count == 1 - assert subscriber2.mock.call_count == 0 + async with self.patch_broker(broker) as br: + await br.start() + await br.publish("", queue) + + assert subscriber1.mock.call_count == 1 + assert subscriber2.mock.call_count == 0 - @pytest.mark.kafka - async def test_broker_gets_patched_attrs_within_cm(self): - await super().test_broker_gets_patched_attrs_within_cm() + @pytest.mark.kafka() + async def test_broker_gets_patched_attrs_within_cm(self) -> None: + await super().test_broker_gets_patched_attrs_within_cm(FakeProducer) - @pytest.mark.kafka - async def test_broker_with_real_doesnt_get_patched(self): + @pytest.mark.kafka() + async def test_broker_with_real_doesnt_get_patched(self) -> None: await super().test_broker_with_real_doesnt_get_patched() - @pytest.mark.kafka + @pytest.mark.kafka() async def test_broker_with_real_patches_publishers_and_subscribers( - self, queue: str - ): + self, + queue: str, + ) -> None: await super().test_broker_with_real_patches_publishers_and_subscribers(queue) diff --git a/tests/brokers/kafka/test_test_reentrancy.py b/tests/brokers/kafka/test_test_reentrancy.py index de4d246be8..4af4df883c 100644 --- a/tests/brokers/kafka/test_test_reentrancy.py +++ b/tests/brokers/kafka/test_test_reentrancy.py @@ -15,11 +15,11 @@ async def on_input_data(msg: int): @broker.subscriber("output_data") -async def on_output_data(msg: int): +async def on_output_data(msg: int) -> None: pass -async def _test_with_broker(with_real: bool): +async def _test_with_broker(with_real: bool) -> None: async with TestKafkaBroker(broker, with_real=with_real) as tester: await tester.publish(1, "input_data") @@ -30,22 +30,22 @@ async def _test_with_broker(with_real: bool): on_output_data.mock.assert_called_once_with(2) -@pytest.mark.asyncio -async def test_with_fake_broker(): +@pytest.mark.asyncio() +async def test_with_fake_broker() -> None: await _test_with_broker(False) await _test_with_broker(False) -@pytest.mark.asyncio -@pytest.mark.kafka -async def test_with_real_broker(): +@pytest.mark.asyncio() +@pytest.mark.kafka() +async def test_with_real_broker() -> None: await _test_with_broker(True) await _test_with_broker(True) -async def _test_with_temp_subscriber(): +async def _test_with_temp_subscriber() -> None: @broker.subscriber("output_data") - async def on_output_data(msg: int): + async def on_output_data(msg: int) -> None: pass async with TestKafkaBroker(broker) as tester: @@ -58,13 +58,13 @@ async def on_output_data(msg: int): on_output_data.mock.assert_called_once_with(2) -@pytest.mark.asyncio +@pytest.mark.asyncio() @pytest.mark.skip( reason=( "Failed due `on_output_data` subscriber creates inside test and doesn't removed after " "https://github.com/ag2ai/faststream/issues/556" ) ) -async def test_with_temp_subscriber(): +async def test_with_temp_subscriber() -> None: await _test_with_temp_subscriber() await _test_with_temp_subscriber() diff --git a/tests/brokers/nats/basic.py b/tests/brokers/nats/basic.py new file mode 100644 index 0000000000..bc73b67da6 --- /dev/null +++ b/tests/brokers/nats/basic.py @@ -0,0 +1,24 @@ +from typing import Any + +from faststream.nats import NatsBroker, NatsRouter, TestNatsBroker +from tests.brokers.base.basic import BaseTestcaseConfig + + +class NatsTestcaseConfig(BaseTestcaseConfig): + def get_broker( + self, + apply_types: bool = False, + **kwargs: Any, + ) -> NatsBroker: + return NatsBroker(apply_types=apply_types, **kwargs) + + def patch_broker(self, broker: NatsBroker, **kwargs: Any) -> NatsBroker: + return broker + + def get_router(self, **kwargs: Any) -> NatsRouter: + return NatsRouter(**kwargs) + + +class NatsMemoryTestcaseConfig(NatsTestcaseConfig): + def patch_broker(self, broker: NatsBroker, **kwargs: Any) -> NatsBroker: + return TestNatsBroker(broker, **kwargs) diff --git a/tests/brokers/nats/conftest.py b/tests/brokers/nats/conftest.py index 253cd709f2..02fbb3c70a 100644 --- a/tests/brokers/nats/conftest.py +++ b/tests/brokers/nats/conftest.py @@ -1,19 +1,13 @@ from dataclasses import dataclass import pytest -import pytest_asyncio -from faststream.nats import ( - JStream, - NatsBroker, - NatsRouter, - TestNatsBroker, -) +from faststream.nats import JStream, NatsRouter @dataclass class Settings: - url = "nats://localhost:4222" # pragma: allowlist secret + url: str = "nats://localhost:4222" # pragma: allowlist secret @pytest.fixture(scope="session") @@ -21,32 +15,11 @@ def settings(): return Settings() -@pytest.fixture +@pytest.fixture() def stream(queue): return JStream(queue) -@pytest.fixture +@pytest.fixture() def router(): return NatsRouter() - - -@pytest_asyncio.fixture() -async def broker(settings): - broker = NatsBroker([settings.url], apply_types=False) - async with broker: - yield broker - - -@pytest_asyncio.fixture() -async def full_broker(settings): - broker = NatsBroker([settings.url]) - async with broker: - yield broker - - -@pytest_asyncio.fixture() -async def test_broker(): - broker = NatsBroker() - async with TestNatsBroker(broker) as br: - yield br diff --git a/tests/brokers/nats/future/test_fastapi.py b/tests/brokers/nats/future/test_fastapi.py index c32f0b4ed6..af3eb2572e 100644 --- a/tests/brokers/nats/future/test_fastapi.py +++ b/tests/brokers/nats/future/test_fastapi.py @@ -1,11 +1,11 @@ import pytest +from faststream.nats.broker.router import NatsRouter from faststream.nats.fastapi import NatsRouter as StreamRouter -from faststream.nats.router import NatsRouter from tests.brokers.base.future.fastapi import FastapiTestCase -@pytest.mark.nats +@pytest.mark.nats() class TestRouter(FastapiTestCase): router_class = StreamRouter broker_router_class = NatsRouter diff --git a/tests/brokers/nats/test_config.py b/tests/brokers/nats/test_config.py new file mode 100644 index 0000000000..81046390f0 --- /dev/null +++ b/tests/brokers/nats/test_config.py @@ -0,0 +1,42 @@ +from faststream import AckPolicy +from faststream.nats import ConsumerConfig +from faststream.nats.subscriber.config import NatsSubscriberConfig + + +def test_default() -> None: + config = NatsSubscriberConfig( + subject="test_subject", + sub_config=ConsumerConfig(), + ) + + assert config.ack_policy is AckPolicy.REJECT_ON_ERROR + + +def test_no_ack() -> None: + config = NatsSubscriberConfig( + subject="test_subject", + sub_config=ConsumerConfig(), + _no_ack=True, + ) + + assert config.ack_policy is AckPolicy.DO_NOTHING + + +def test_ack_first() -> None: + config = NatsSubscriberConfig( + subject="test_subject", + sub_config=ConsumerConfig(), + _ack_first=True, + ) + + assert config.ack_policy is AckPolicy.ACK_FIRST + + +def test_custom_ack() -> None: + config = NatsSubscriberConfig( + subject="test_subject", + sub_config=ConsumerConfig(), + _ack_policy=AckPolicy.ACK, + ) + + assert config.ack_policy is AckPolicy.ACK diff --git a/tests/brokers/nats/test_connect.py b/tests/brokers/nats/test_connect.py index f46e6480a9..dc18feed5c 100644 --- a/tests/brokers/nats/test_connect.py +++ b/tests/brokers/nats/test_connect.py @@ -4,7 +4,7 @@ from tests.brokers.base.connection import BrokerConnectionTestcase -@pytest.mark.nats +@pytest.mark.nats() class TestConnection(BrokerConnectionTestcase): broker = NatsBroker diff --git a/tests/brokers/nats/test_consume.py b/tests/brokers/nats/test_consume.py index eececb8b18..025f03a68e 100644 --- a/tests/brokers/nats/test_consume.py +++ b/tests/brokers/nats/test_consume.py @@ -1,58 +1,105 @@ import asyncio -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, patch import pytest from nats.aio.msg import Msg +from faststream import AckPolicy from faststream.exceptions import AckMessage -from faststream.nats import ConsumerConfig, JStream, NatsBroker, PullSub +from faststream.nats import ConsumerConfig, JStream, PubAck, PullSub from faststream.nats.annotations import NatsMessage +from faststream.nats.message import NatsMessage as StreamMessage from tests.brokers.base.consume import BrokerRealConsumeTestcase from tests.tools import spy_decorator +from .basic import NatsTestcaseConfig -@pytest.mark.nats -class TestConsume(BrokerRealConsumeTestcase): - def get_broker(self, apply_types: bool = False) -> NatsBroker: - return NatsBroker(apply_types=apply_types) + +@pytest.mark.nats() +class TestConsume(NatsTestcaseConfig, BrokerRealConsumeTestcase): + async def test_concurrent_subscriber( + self, + queue: str, + mock: MagicMock, + ) -> None: + event = asyncio.Event() + event2 = asyncio.Event() + + broker = self.get_broker() + + args, kwargs = self.get_subscriber_params(queue, max_workers=2) + + @broker.subscriber(*args, **kwargs) + async def handler(msg): + mock() + + if event.is_set(): + event2.set() + else: + event.set() + + await asyncio.sleep(1.0) + + async with self.patch_broker(broker) as br: + await br.start() + + for i in range(5): + await br.publish(i, queue) + + await asyncio.wait( + ( + asyncio.create_task(event.wait()), + asyncio.create_task(event2.wait()), + ), + timeout=3, + ) + + assert event.is_set() + assert event2.is_set() + assert mock.call_count == 2, mock.call_count async def test_consume_js( self, queue: str, stream: JStream, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() - @consume_broker.subscriber(queue, stream=stream) - def subscriber(m): + args, kwargs = self.get_subscriber_params(queue, stream=stream) + + @consume_broker.subscriber(*args, **kwargs) + def subscriber(m) -> None: event.set() async with self.patch_broker(consume_broker) as br: await br.start() + + result = await br.publish("hello", queue, stream=stream.name) + await asyncio.wait( - ( - asyncio.create_task(br.publish("hello", queue, stream=stream.name)), - asyncio.create_task(event.wait()), - ), + (asyncio.create_task(event.wait()),), timeout=3, ) + assert isinstance(result, PubAck), result assert event.is_set() async def test_consume_with_filter( self, - queue, - mock: Mock, - event: asyncio.Event, - ): + queue: str, + mock: MagicMock, + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber( config=ConsumerConfig(filter_subjects=[f"{queue}.a"]), stream=JStream(queue, subjects=[f"{queue}.*"]), ) - def subscriber(m): + def subscriber(m) -> None: mock(m) event.set() @@ -73,9 +120,10 @@ async def test_consume_pull( self, queue: str, stream: JStream, - event: asyncio.Event, mock, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber( @@ -83,7 +131,7 @@ async def test_consume_pull( stream=stream, pull_sub=PullSub(1), ) - def subscriber(m): + def subscriber(m) -> None: mock(m) event.set() @@ -105,9 +153,10 @@ async def test_consume_batch( self, queue: str, stream: JStream, - event: asyncio.Event, mock, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber( @@ -115,7 +164,7 @@ async def test_consume_batch( stream=stream, pull_sub=PullSub(1, batch=True), ) - def subscriber(m): + def subscriber(m) -> None: mock(m) event.set() @@ -133,22 +182,31 @@ def subscriber(m): assert event.is_set() mock.assert_called_once_with([b"hello"]) - async def test_consume_ack( + async def test_core_consume_no_ack( self, queue: str, - event: asyncio.Event, - stream: JStream, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue, stream=stream) - async def handler(msg: NatsMessage): + args, kwargs = self.get_subscriber_params( + queue, ack_policy=AckPolicy.DO_NOTHING + ) + + @consume_broker.subscriber(*args, **kwargs) + async def handler(msg: NatsMessage) -> None: + mock(msg.raw_message._ackd) event.set() async with self.patch_broker(consume_broker) as br: await br.start() - with patch.object(Msg, "ack", spy_decorator(Msg.ack)) as m: + # Check, that Core Subscriber doesn't call Acknowledgement automatically + with patch.object( + StreamMessage, "ack", spy_decorator(StreamMessage.ack) + ) as m: await asyncio.wait( ( asyncio.create_task(br.publish("hello", queue)), @@ -156,46 +214,50 @@ async def handler(msg: NatsMessage): ), timeout=3, ) - m.mock.assert_called_once() + assert not m.mock.called assert event.is_set() + mock.assert_called_once_with(False) - async def test_core_consume_no_ack( + async def test_consume_ack( self, queue: str, - event: asyncio.Event, stream: JStream, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue, no_ack=True) - async def handler(msg: NatsMessage): - if not msg.raw_message._ackd: - event.set() + @consume_broker.subscriber(queue, stream=stream) + async def handler(msg: NatsMessage) -> None: + event.set() async with self.patch_broker(consume_broker) as br: await br.start() - await asyncio.wait( - ( - asyncio.create_task(br.publish("hello", queue)), - asyncio.create_task(event.wait()), - ), - timeout=3, - ) + with patch.object(Msg, "ack", spy_decorator(Msg.ack)) as m: + await asyncio.wait( + ( + asyncio.create_task(br.publish("hello", queue)), + asyncio.create_task(event.wait()), + ), + timeout=3, + ) + m.mock.assert_called_once() assert event.is_set() async def test_consume_ack_manual( self, queue: str, - event: asyncio.Event, stream: JStream, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber(queue, stream=stream) - async def handler(msg: NatsMessage): + async def handler(msg: NatsMessage) -> None: await msg.ack() event.set() @@ -245,15 +307,16 @@ async def handler(msg: NatsMessage): async def test_consume_ack_raise( self, queue: str, - event: asyncio.Event, stream: JStream, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber(queue, stream=stream) async def handler(msg: NatsMessage): event.set() - raise AckMessage() + raise AckMessage async with self.patch_broker(consume_broker) as br: await br.start() @@ -273,13 +336,14 @@ async def handler(msg: NatsMessage): async def test_nack( self, queue: str, - event: asyncio.Event, stream: JStream, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber(queue, stream=stream) - async def handler(msg: NatsMessage): + async def handler(msg: NatsMessage) -> None: await msg.nack() event.set() @@ -301,12 +365,16 @@ async def handler(msg: NatsMessage): async def test_consume_no_ack( self, queue: str, - event: asyncio.Event, - ): + stream: str, + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue, no_ack=True) - async def handler(msg: NatsMessage): + @consume_broker.subscriber( + queue, stream=stream, ack_policy=AckPolicy.DO_NOTHING + ) + async def handler(msg: NatsMessage) -> None: event.set() async with self.patch_broker(consume_broker) as br: @@ -328,9 +396,10 @@ async def test_consume_batch_headers( self, queue: str, stream: JStream, - event: asyncio.Event, mock, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber( @@ -338,13 +407,13 @@ async def test_consume_batch_headers( stream=stream, pull_sub=PullSub(1, batch=True), ) - def subscriber(m, msg: NatsMessage): + def subscriber(m, msg: NatsMessage) -> None: check = all( ( msg.headers, [msg.headers] == msg.batch_headers, msg.headers.get("custom") == "1", - ) + ), ) mock(check) event.set() @@ -362,17 +431,18 @@ def subscriber(m, msg: NatsMessage): assert event.is_set() mock.assert_called_once_with(True) - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_kv( self, queue: str, - event: asyncio.Event, mock, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber(queue, kv_watch=queue + "1") - async def handler(m): + async def handler(m) -> None: mock(m) event.set() @@ -386,7 +456,7 @@ async def handler(m): bucket.put( queue, b"world", - ) + ), ), asyncio.create_task(event.wait()), ), @@ -396,17 +466,18 @@ async def handler(m): assert event.is_set() mock.assert_called_with(b"world") - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_os( self, queue: str, - event: asyncio.Event, mock, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber(queue, obj_watch=True) - async def handler(filename: str): + async def handler(filename: str) -> None: event.set() mock(filename) @@ -420,7 +491,7 @@ async def handler(filename: str): bucket.put( "hello", b"world", - ) + ), ), asyncio.create_task(event.wait()), ), @@ -433,9 +504,8 @@ async def handler(filename: str): async def test_get_one_js( self, queue: str, - event: asyncio.Event, stream: JStream, - ): + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber(queue, stream=stream) @@ -444,11 +514,11 @@ async def test_get_one_js( message = None - async def consume(): + async def consume() -> None: nonlocal message message = await subscriber.get_one(timeout=5) - async def publish(): + async def publish() -> None: await br.publish("test_message", queue, stream=stream.name) await asyncio.wait( @@ -467,7 +537,7 @@ async def test_get_one_timeout_js( queue: str, stream: JStream, mock, - ): + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber(queue, stream=stream) @@ -480,9 +550,8 @@ async def test_get_one_timeout_js( async def test_get_one_pull( self, queue: str, - event: asyncio.Event, stream: JStream, - ): + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber( queue, @@ -495,11 +564,11 @@ async def test_get_one_pull( message = None - async def consume(): + async def consume() -> None: nonlocal message message = await subscriber.get_one(timeout=5) - async def publish(): + async def publish() -> None: await br.publish("test_message", queue) await asyncio.wait( @@ -516,10 +585,9 @@ async def publish(): async def test_get_one_pull_timeout( self, queue: str, - event: asyncio.Event, stream: JStream, - mock: Mock, - ): + mock: MagicMock, + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber( queue, @@ -536,9 +604,8 @@ async def test_get_one_pull_timeout( async def test_get_one_batch( self, queue: str, - event: asyncio.Event, stream: JStream, - ): + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber( queue, @@ -551,11 +618,11 @@ async def test_get_one_batch( message = None - async def consume(): + async def consume() -> None: nonlocal message message = await subscriber.get_one(timeout=5) - async def publish(): + async def publish() -> None: await br.publish("test_message", queue) await asyncio.wait( @@ -572,10 +639,9 @@ async def publish(): async def test_get_one_batch_timeout( self, queue: str, - event: asyncio.Event, stream: JStream, - mock: Mock, - ): + mock: MagicMock, + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber( queue, @@ -592,9 +658,8 @@ async def test_get_one_batch_timeout( async def test_get_one_with_filter( self, queue: str, - event: asyncio.Event, stream: JStream, - ): + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber( config=ConsumerConfig(filter_subjects=[f"{queue}.a"]), @@ -606,11 +671,11 @@ async def test_get_one_with_filter( message = None - async def consume(): + async def consume() -> None: nonlocal message message = await subscriber.get_one(timeout=5) - async def publish(): + async def publish() -> None: await br.publish("test_message", f"{queue}.a") await asyncio.wait( @@ -627,9 +692,8 @@ async def publish(): async def test_get_one_kv( self, queue: str, - event: asyncio.Event, stream: JStream, - ): + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber(queue, kv_watch=queue + "1") @@ -639,11 +703,11 @@ async def test_get_one_kv( message = None - async def consume(): + async def consume() -> None: nonlocal message message = await subscriber.get_one(timeout=5) - async def publish(): + async def publish() -> None: await bucket.put(queue, b"test_message") await asyncio.wait( @@ -660,10 +724,9 @@ async def publish(): async def test_get_one_kv_timeout( self, queue: str, - event: asyncio.Event, stream: JStream, - mock: Mock, - ): + mock: MagicMock, + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber(queue, kv_watch=queue + "1") @@ -676,9 +739,8 @@ async def test_get_one_kv_timeout( async def test_get_one_os( self, queue: str, - event: asyncio.Event, stream: JStream, - ): + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber(queue, obj_watch=True) @@ -688,12 +750,12 @@ async def test_get_one_os( new_object_id = None - async def consume(): + async def consume() -> None: nonlocal new_object_id new_object_event = await subscriber.get_one(timeout=5) new_object_id = await new_object_event.decode() - async def publish(): + async def publish() -> None: await bucket.put(queue, b"test_message") await asyncio.wait( @@ -710,10 +772,9 @@ async def publish(): async def test_get_one_os_timeout( self, queue: str, - event: asyncio.Event, stream: JStream, - mock: Mock, - ): + mock: MagicMock, + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber(queue, obj_watch=True) @@ -722,3 +783,190 @@ async def test_get_one_os_timeout( mock(await subscriber.get_one(timeout=1e-24)) mock.assert_called_once_with(None) + + async def test_iterator_js( + self, + queue: str, + stream: JStream, + ) -> None: + expected_messages = ("test_message_1", "test_message_2") + + broker = self.get_broker(apply_types=True) + subscriber = broker.subscriber(queue, stream=stream) + + async with self.patch_broker(broker) as br: + await br.start() + + async def publish_test_message(): + for msg in expected_messages: + await br.publish(msg, queue) + + _ = await asyncio.create_task(publish_test_message()) + + index_message = 0 + async for msg in subscriber: + result_message = await msg.decode() + + assert result_message == expected_messages[index_message] + + index_message += 1 + if index_message >= len(expected_messages): + break + + async def test_iterator_pull( + self, + queue: str, + stream: JStream, + ) -> None: + expected_messages = ("test_message_1", "test_message_2") + + broker = self.get_broker(apply_types=True) + subscriber = broker.subscriber( + queue, + stream=stream, + pull_sub=PullSub(1), + ) + + async with self.patch_broker(broker) as br: + await br.start() + + async def publish_test_message(): + for msg in expected_messages: + await br.publish(msg, queue) + + _ = await asyncio.create_task(publish_test_message()) + + index_message = 0 + async for msg in subscriber: + result_message = await msg.decode() + + assert result_message == expected_messages[index_message] + + index_message += 1 + if index_message >= len(expected_messages): + break + + async def test_iterator_batch( + self, + queue: str, + stream: JStream, + ) -> None: + expected_messages = ("test_message_1", "test_message_2") + + broker = self.get_broker(apply_types=True) + subscriber = broker.subscriber( + queue, + stream=stream, + pull_sub=PullSub(1, batch=True), + ) + + async with self.patch_broker(broker) as br: + await br.start() + + async def publish_test_message(): + for msg in expected_messages: + await br.publish(msg, queue) + + _ = await asyncio.create_task(publish_test_message()) + + index_message = 0 + async for msg in subscriber: + result_message = await msg.decode() + + assert result_message == [expected_messages[index_message]] + + index_message += 1 + if index_message >= len(expected_messages): + break + + async def test_iterator_with_filter( + self, + queue: str, + ) -> None: + expected_messages = ("test_message_1", "test_message_2") + + broker = self.get_broker(apply_types=True) + subscriber = broker.subscriber( + config=ConsumerConfig(filter_subjects=[f"{queue}.a"]), + stream=JStream(queue, subjects=[f"{queue}.*"]), + ) + + async with self.patch_broker(broker) as br: + await br.start() + + async def publish_test_message(): + for msg in expected_messages: + await br.publish(msg, f"{queue}.a") + + _ = await asyncio.create_task(publish_test_message()) + + index_message = 0 + async for msg in subscriber: + result_message = await msg.decode() + + assert result_message == expected_messages[index_message] + + index_message += 1 + if index_message >= len(expected_messages): + break + + async def test_iterator_kv( + self, + queue: str, + ) -> None: + expected_messages = (b"test_message_1", b"test_message_2") + + broker = self.get_broker(apply_types=True) + subscriber = broker.subscriber(queue, kv_watch=queue + "1") + + async with self.patch_broker(broker) as br: + await br.start() + bucket = await br.key_value(queue + "1") + + async def publish_test_message(): + await bucket.put(queue, expected_messages[0]) + + _ = await asyncio.create_task(publish_test_message()) + + index_message = 0 + async for msg in subscriber: + result_message = await msg.decode() + + assert result_message == expected_messages[index_message] + + index_message += 1 + if index_message >= len(expected_messages): + break + + await bucket.put(queue, expected_messages[index_message]) + + async def test_iterator_os( + self, + queue: str, + ) -> None: + expected_messages = (b"test_message_1", b"test_message_2") + + broker = self.get_broker(apply_types=True) + subscriber = broker.subscriber(queue, obj_watch=True) + + async with self.patch_broker(broker) as br: + await br.start() + bucket = await br.object_storage(queue) + + async def publish_test_message(): + await bucket.put(queue, expected_messages[0]) + + _ = await asyncio.create_task(publish_test_message()) + + index_message = 0 + async for new_object_event in subscriber: + new_object_id = await new_object_event.decode() + new_object = await bucket.get(new_object_id) + + assert new_object.data == expected_messages[index_message] + + index_message += 1 + if index_message >= len(expected_messages): + break + + await bucket.put(queue, expected_messages[index_message]) diff --git a/tests/brokers/nats/test_fastapi.py b/tests/brokers/nats/test_fastapi.py index 518fdcd637..15fbe319f7 100644 --- a/tests/brokers/nats/test_fastapi.py +++ b/tests/brokers/nats/test_fastapi.py @@ -1,30 +1,31 @@ import asyncio -from typing import List from unittest.mock import MagicMock import pytest from faststream.nats import JStream, NatsRouter, PullSub from faststream.nats.fastapi import NatsRouter as StreamRouter -from faststream.nats.testing import TestNatsBroker, build_message from tests.brokers.base.fastapi import FastAPILocalTestcase, FastAPITestcase +from .basic import NatsMemoryTestcaseConfig, NatsTestcaseConfig -@pytest.mark.nats -class TestRouter(FastAPITestcase): + +@pytest.mark.nats() +class TestRouter(NatsTestcaseConfig, FastAPITestcase): router_class = StreamRouter broker_router_class = NatsRouter async def test_path( self, queue: str, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() - @router.subscriber("in.{name}") - def subscriber(msg: str, name: str): + @router.subscriber(queue + ".{name}") + def subscriber(msg: str, name: str) -> None: mock(msg=msg, name=name) event.set() @@ -32,7 +33,9 @@ def subscriber(msg: str, name: str): await router.broker.start() await asyncio.wait( ( - asyncio.create_task(router.broker.publish("hello", "in.john")), + asyncio.create_task( + router.broker.publish("hello", f"{queue}.john"), + ), asyncio.create_task(event.wait()), ), timeout=3, @@ -45,9 +48,10 @@ async def test_consume_batch( self, queue: str, stream: JStream, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() @router.subscriber( @@ -55,7 +59,7 @@ async def test_consume_batch( stream=stream, pull_sub=PullSub(1, batch=True), ) - def subscriber(m: List[str]): + def subscriber(m: list[str]) -> None: mock(m) event.set() @@ -73,19 +77,18 @@ def subscriber(m: List[str]): mock.assert_called_once_with(["hello"]) -class TestRouterLocal(FastAPILocalTestcase): +class TestRouterLocal(NatsMemoryTestcaseConfig, FastAPILocalTestcase): router_class = StreamRouter broker_router_class = NatsRouter - broker_test = staticmethod(TestNatsBroker) - build_message = staticmethod(build_message) async def test_consume_batch( self, queue: str, stream: JStream, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() @router.subscriber( @@ -93,14 +96,14 @@ async def test_consume_batch( stream=stream, pull_sub=PullSub(1, batch=True), ) - def subscriber(m: List[str]): + def subscriber(m: list[str]) -> None: mock(m) event.set() - async with self.broker_test(router.broker): + async with self.patch_broker(router.broker) as br: await asyncio.wait( ( - asyncio.create_task(router.broker.publish(b"hello", queue)), + asyncio.create_task(br.publish(b"hello", queue)), asyncio.create_task(event.wait()), ), timeout=3, @@ -109,18 +112,17 @@ def subscriber(m: List[str]): assert event.is_set() mock.assert_called_once_with(["hello"]) - async def test_path(self, queue: str): + async def test_path(self, queue: str) -> None: router = self.router_class() @router.subscriber(queue + ".{name}") async def hello(name): return name - async with self.broker_test(router.broker): - r = await router.broker.publish( + async with self.patch_broker(router.broker) as br: + r = await br.request( "hi", f"{queue}.john", - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "john" + assert await r.decode() == "john" diff --git a/tests/brokers/nats/test_include_router.py b/tests/brokers/nats/test_include_router.py new file mode 100644 index 0000000000..a87247263e --- /dev/null +++ b/tests/brokers/nats/test_include_router.py @@ -0,0 +1,14 @@ +from tests.brokers.base.include_router import ( + IncludePublisherTestcase, + IncludeSubscriberTestcase, +) + +from .basic import NatsTestcaseConfig + + +class TestSubscriber(NatsTestcaseConfig, IncludeSubscriberTestcase): + pass + + +class TestPublisher(NatsTestcaseConfig, IncludePublisherTestcase): + pass diff --git a/tests/brokers/nats/test_kv_declarer_cache.py b/tests/brokers/nats/test_kv_declarer_cache.py index 8ecc9fcce6..cf3c4a9ed9 100644 --- a/tests/brokers/nats/test_kv_declarer_cache.py +++ b/tests/brokers/nats/test_kv_declarer_cache.py @@ -7,9 +7,9 @@ from tests.tools import spy_decorator -@pytest.mark.asyncio -@pytest.mark.nats -async def test_kv_storage_cache(): +@pytest.mark.asyncio() +@pytest.mark.nats() +async def test_kv_storage_cache() -> None: broker = NatsBroker() await broker.connect() with patch.object( @@ -19,5 +19,5 @@ async def test_kv_storage_cache(): ) as m: await broker.key_value("test") await broker.key_value("test") - assert broker._kv_declarer.buckets["test"] + assert broker.config.kv_declarer.buckets["test"] m.mock.assert_called_once() diff --git a/tests/brokers/nats/test_middlewares.py b/tests/brokers/nats/test_middlewares.py index d9dbca9224..c726d7e231 100644 --- a/tests/brokers/nats/test_middlewares.py +++ b/tests/brokers/nats/test_middlewares.py @@ -1,25 +1,23 @@ import pytest -from faststream.nats import NatsBroker, TestNatsBroker from tests.brokers.base.middlewares import ( ExceptionMiddlewareTestcase, MiddlewareTestcase, MiddlewaresOrderTestcase, ) +from .basic import NatsMemoryTestcaseConfig, NatsTestcaseConfig -@pytest.mark.nats -class TestMiddlewares(MiddlewareTestcase): - broker_class = NatsBroker +class TestMiddlewaresOrder(NatsMemoryTestcaseConfig, MiddlewaresOrderTestcase): + pass -@pytest.mark.nats -class TestExceptionMiddlewares(ExceptionMiddlewareTestcase): - broker_class = NatsBroker +@pytest.mark.nats() +class TestMiddlewares(NatsTestcaseConfig, MiddlewareTestcase): + pass -class TestMiddlewaresOrder(MiddlewaresOrderTestcase): - broker_class = NatsBroker - def patch_broker(self, broker: NatsBroker) -> TestNatsBroker: - return TestNatsBroker(broker) +@pytest.mark.nats() +class TestExceptionMiddlewares(NatsTestcaseConfig, ExceptionMiddlewareTestcase): + pass diff --git a/tests/brokers/nats/test_misconfiguration.py b/tests/brokers/nats/test_misconfiguration.py index 0f88f83cb0..6a9716d3d0 100644 --- a/tests/brokers/nats/test_misconfiguration.py +++ b/tests/brokers/nats/test_misconfiguration.py @@ -1,7 +1,6 @@ import pytest from faststream.exceptions import SetupError -from faststream.kafka import KafkaRouter from faststream.nats import NatsRouter from faststream.nats.broker.broker import NatsBroker from faststream.rabbit import RabbitRouter @@ -14,7 +13,7 @@ def test_use_only_nats_router() -> None: with pytest.raises(SetupError): broker.include_router(router) - routers = [NatsRouter(), RabbitRouter(), KafkaRouter()] + routers = [NatsRouter(), RabbitRouter()] with pytest.raises(SetupError): broker.include_routers(routers) diff --git a/tests/brokers/nats/test_new_inbox.py b/tests/brokers/nats/test_new_inbox.py index ce6db83ee0..b608a1f6ef 100644 --- a/tests/brokers/nats/test_new_inbox.py +++ b/tests/brokers/nats/test_new_inbox.py @@ -7,9 +7,9 @@ from tests.tools import spy_decorator -@pytest.mark.asyncio -@pytest.mark.nats -async def test_new_inbox(): +@pytest.mark.asyncio() +@pytest.mark.nats() +async def test_new_inbox() -> None: with patch.object( NatsClient, "new_inbox", diff --git a/tests/brokers/nats/test_os_declarer_cache.py b/tests/brokers/nats/test_os_declarer_cache.py index 0f68542c8e..7e8fdf6f96 100644 --- a/tests/brokers/nats/test_os_declarer_cache.py +++ b/tests/brokers/nats/test_os_declarer_cache.py @@ -7,9 +7,9 @@ from tests.tools import spy_decorator -@pytest.mark.asyncio -@pytest.mark.nats -async def test_object_storage_cache(): +@pytest.mark.asyncio() +@pytest.mark.nats() +async def test_object_storage_cache() -> None: broker = NatsBroker() await broker.connect() @@ -20,5 +20,5 @@ async def test_object_storage_cache(): ) as m: await broker.object_storage("test") await broker.object_storage("test") - assert broker._os_declarer.buckets["test"] + assert broker.config.os_declarer.buckets["test"] m.mock.assert_called_once() diff --git a/tests/brokers/nats/test_parser.py b/tests/brokers/nats/test_parser.py index a50b4d4d18..635cbccb65 100644 --- a/tests/brokers/nats/test_parser.py +++ b/tests/brokers/nats/test_parser.py @@ -1,9 +1,10 @@ import pytest -from faststream.nats import NatsBroker from tests.brokers.base.parser import CustomParserTestcase +from .basic import NatsTestcaseConfig -@pytest.mark.nats -class TestCustomParser(CustomParserTestcase): - broker_class = NatsBroker + +@pytest.mark.nats() +class TestCustomParser(NatsTestcaseConfig, CustomParserTestcase): + pass diff --git a/tests/brokers/nats/test_publish.py b/tests/brokers/nats/test_publish.py index 1fb8b799d6..015b03d645 100644 --- a/tests/brokers/nats/test_publish.py +++ b/tests/brokers/nats/test_publish.py @@ -1,27 +1,27 @@ import asyncio -from unittest.mock import Mock +from unittest.mock import MagicMock import pytest from faststream import Context -from faststream.nats import NatsBroker, NatsResponse +from faststream.nats import NatsResponse from tests.brokers.base.publish import BrokerPublishTestcase +from .basic import NatsTestcaseConfig -@pytest.mark.nats -class TestPublish(BrokerPublishTestcase): - """Test publish method of NATS broker.""" - def get_broker(self, apply_types: bool = False) -> NatsBroker: - return NatsBroker(apply_types=apply_types) +@pytest.mark.nats() +class TestPublish(NatsTestcaseConfig, BrokerPublishTestcase): + """Test publish method of NATS broker.""" - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_response( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) @pub_broker.subscriber(queue) @@ -30,7 +30,7 @@ async def handle(): return NatsResponse(1, correlation_id="1") @pub_broker.subscriber(queue + "1") - async def handle_next(msg=Context("message")): + async def handle_next(msg=Context("message")) -> None: mock( body=msg.body, correlation_id=msg.correlation_id, @@ -54,12 +54,11 @@ async def handle_next(msg=Context("message")): correlation_id="1", ) - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_response_for_rpc( self, queue: str, - event: asyncio.Event, - ): + ) -> None: pub_broker = self.get_broker(apply_types=True) @pub_broker.subscriber(queue) @@ -70,8 +69,8 @@ async def handle(): await br.start() response = await asyncio.wait_for( - br.publish("", queue, rpc=True), + br.request("", queue), timeout=3, ) - assert response == "Hi!", response + assert await response.decode() == "Hi!", response diff --git a/tests/brokers/nats/test_requests.py b/tests/brokers/nats/test_requests.py index 19f9c2cb15..f440a83c6d 100644 --- a/tests/brokers/nats/test_requests.py +++ b/tests/brokers/nats/test_requests.py @@ -1,31 +1,26 @@ import pytest from faststream import BaseMiddleware -from faststream.nats import NatsBroker, NatsRouter, TestNatsBroker from tests.brokers.base.requests import RequestsTestcase +from .basic import NatsMemoryTestcaseConfig, NatsTestcaseConfig + class Mid(BaseMiddleware): async def on_receive(self) -> None: - self.msg.data = self.msg.data * 2 + self.msg.data *= 2 async def consume_scope(self, call_next, msg): - msg._decoded_body = msg._decoded_body * 2 + msg.body *= 2 return await call_next(msg) -@pytest.mark.asyncio +@pytest.mark.asyncio() class NatsRequestsTestcase(RequestsTestcase): def get_middleware(self, **kwargs): return Mid - def get_broker(self, **kwargs): - return NatsBroker(**kwargs) - - def get_router(self, **kwargs): - return NatsRouter(**kwargs) - - async def test_broker_stream_request(self, queue: str): + async def test_broker_stream_request(self, queue: str) -> None: broker = self.get_broker() stream_name = f"{queue}st" @@ -33,7 +28,7 @@ async def test_broker_stream_request(self, queue: str): args, kwargs = self.get_subscriber_params(queue, stream=stream_name) @broker.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg) -> str: return "Response" async with self.patch_broker(broker): @@ -50,7 +45,7 @@ async def handler(msg): assert await response.decode() == "Response" assert response.correlation_id == "1" - async def test_publisher_stream_request(self, queue: str): + async def test_publisher_stream_request(self, queue: str) -> None: broker = self.get_broker() stream_name = f"{queue}st" @@ -59,7 +54,7 @@ async def test_publisher_stream_request(self, queue: str): args, kwargs = self.get_subscriber_params(queue, stream=stream_name) @broker.subscriber(*args, **kwargs) - async def handler(msg): + async def handler(msg) -> str: return "Response" async with self.patch_broker(broker): @@ -75,11 +70,10 @@ async def handler(msg): assert response.correlation_id == "1" -@pytest.mark.nats -class TestRealRequests(NatsRequestsTestcase): +@pytest.mark.nats() +class TestRealRequests(NatsTestcaseConfig, NatsRequestsTestcase): pass -class TestRequestTestClient(NatsRequestsTestcase): - def patch_broker(self, broker, **kwargs): - return TestNatsBroker(broker, **kwargs) +class TestRequestTestClient(NatsMemoryTestcaseConfig, NatsRequestsTestcase): + pass diff --git a/tests/brokers/nats/test_router.py b/tests/brokers/nats/test_router.py index 24b3fd772e..84be0c9578 100644 --- a/tests/brokers/nats/test_router.py +++ b/tests/brokers/nats/test_router.py @@ -1,144 +1,140 @@ import asyncio +from unittest.mock import MagicMock import pytest from faststream import Path -from faststream.nats import JStream, NatsBroker, NatsPublisher, NatsRoute, NatsRouter +from faststream.nats import ( + JStream, + NatsPublisher, + NatsRoute, + NatsRouter, +) from tests.brokers.base.router import RouterLocalTestcase, RouterTestcase +from .basic import NatsMemoryTestcaseConfig, NatsTestcaseConfig -@pytest.mark.nats -class TestRouter(RouterTestcase): - broker_class = NatsRouter + +@pytest.mark.nats() +class TestRouter(NatsTestcaseConfig, RouterTestcase): route_class = NatsRoute publisher_class = NatsPublisher async def test_router_path( self, - event, - mock, + event: asyncio.Event, + mock: MagicMock, router: NatsRouter, - pub_broker, - ): + ) -> None: + pub_broker = self.get_broker(apply_types=True) + @router.subscriber("in.{name}.{id}") async def h( name: str = Path(), id: int = Path("id"), - ): + ) -> None: event.set() mock(name=name, id=id) - pub_broker._is_apply_types = True pub_broker.include_router(router) await pub_broker.start() - await pub_broker.publish( - "", - "in.john.2", - rpc=True, - ) + await pub_broker.request("", "in.john.2") assert event.is_set() mock.assert_called_once_with(name="john", id=2) async def test_path_as_first_with_prefix( self, - event, - mock, - router: NatsRouter, - pub_broker, - ): - router.prefix = "root." + event: asyncio.Event, + mock: MagicMock, + ) -> None: + pub_broker = self.get_broker(apply_types=True) + + router = self.get_router(prefix="root.") @router.subscriber("{name}.nested") - async def h(name: str = Path()): + async def h(name: str = Path()) -> None: event.set() mock(name=name) - pub_broker._is_apply_types = True pub_broker.include_router(router) await pub_broker.start() - await pub_broker.publish("", "root.john.nested", rpc=True) + await pub_broker.request("", "root.john.nested") assert event.is_set() mock.assert_called_once_with(name="john") async def test_router_path_with_prefix( self, - event, - mock, - router: NatsRouter, - pub_broker, - ): - router.prefix = "test." + event: asyncio.Event, + mock: MagicMock, + ) -> None: + pub_broker = self.get_broker(apply_types=True) + + router = self.get_router(prefix="test.") @router.subscriber("in.{name}.{id}") async def h( name: str = Path(), id: int = Path("id"), - ): + ) -> None: event.set() mock(name=name, id=id) - pub_broker._is_apply_types = True pub_broker.include_router(router) await pub_broker.start() - await pub_broker.publish( - "", - "test.in.john.2", - rpc=True, - ) + await pub_broker.request("", "test.in.john.2") assert event.is_set() mock.assert_called_once_with(name="john", id=2) async def test_router_delay_handler_path( self, - event, - mock, + event: asyncio.Event, + mock: MagicMock, router: NatsRouter, - pub_broker, - ): + ) -> None: + pub_broker = self.get_broker(apply_types=True) + async def h( name: str = Path(), id: int = Path("id"), - ): + ) -> None: event.set() mock(name=name, id=id) r = type(router)(handlers=(self.route_class(h, subject="in.{name}.{id}"),)) - pub_broker._is_apply_types = True pub_broker.include_router(r) await pub_broker.start() - await pub_broker.publish( - "", - "in.john.2", - rpc=True, - ) + await pub_broker.request("", "in.john.2") assert event.is_set() mock.assert_called_once_with(name="john", id=2) async def test_delayed_handlers_with_queue( self, - event, router: NatsRouter, queue: str, - pub_broker, - ): - def response(m): + ) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + + def response(m) -> None: event.set() r = type(router)( - prefix="test.", handlers=(self.route_class(response, subject=queue),) + prefix="test.", + handlers=(self.route_class(response, subject=queue),), ) pub_broker.include_router(r) @@ -156,24 +152,24 @@ def response(m): assert event.is_set() -class TestRouterLocal(RouterLocalTestcase): - broker_class = NatsRouter +class TestRouterLocal(NatsMemoryTestcaseConfig, RouterLocalTestcase): route_class = NatsRoute publisher_class = NatsPublisher async def test_include_stream( self, router: NatsRouter, - pub_broker: NatsBroker, - ): + ) -> None: + pub_broker = self.get_broker() + @router.subscriber("test", stream="stream") - async def handler(): ... + async def handler() -> None: ... pub_broker.include_router(router) assert next(iter(pub_broker._stream_builder.objects.keys())) == "stream" - async def test_include_stream_with_subjects(self): + async def test_include_stream_with_subjects(self) -> None: stream = JStream("test-stream") sub_router = NatsRouter(prefix="client.") @@ -187,7 +183,7 @@ async def test_include_stream_with_subjects(self): router.include_router(sub_router) - broker = NatsBroker() + broker = self.get_broker() broker.include_router(router) assert set(stream.subjects) == { diff --git a/tests/brokers/nats/test_rpc.py b/tests/brokers/nats/test_rpc.py deleted file mode 100644 index d863008fb4..0000000000 --- a/tests/brokers/nats/test_rpc.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytest - -from faststream.nats import JStream, NatsBroker -from tests.brokers.base.rpc import BrokerRPCTestcase, ReplyAndConsumeForbidden - - -@pytest.mark.nats -class TestRPC(BrokerRPCTestcase, ReplyAndConsumeForbidden): - def get_broker(self, apply_types: bool = False) -> NatsBroker: - return NatsBroker(apply_types=apply_types) - - @pytest.mark.asyncio - async def test_rpc_js(self, queue: str, stream: JStream): - rpc_broker = self.get_broker() - - @rpc_broker.subscriber(queue, stream=stream) - async def m(m): # pragma: no cover - return "1" - - async with rpc_broker: - await rpc_broker.start() - - r = await rpc_broker.publish( - "hello", queue, rpc_timeout=3, stream=stream.name, rpc=True - ) - assert r == "1" diff --git a/tests/brokers/nats/test_test_client.py b/tests/brokers/nats/test_test_client.py index b8f7f8d5b2..1c008ed9b2 100644 --- a/tests/brokers/nats/test_test_client.py +++ b/tests/brokers/nats/test_test_client.py @@ -3,77 +3,61 @@ import pytest from faststream import BaseMiddleware -from faststream.exceptions import SetupError -from faststream.nats import ConsumerConfig, JStream, NatsBroker, PullSub, TestNatsBroker +from faststream.nats import ( + ConsumerConfig, + JStream, + PullSub, +) from faststream.nats.testing import FakeProducer from tests.brokers.base.testclient import BrokerTestclientTestcase +from .basic import NatsMemoryTestcaseConfig -@pytest.mark.asyncio -class TestTestclient(BrokerTestclientTestcase): - test_class = TestNatsBroker - def get_broker(self, apply_types: bool = False) -> NatsBroker: - return NatsBroker(apply_types=apply_types) - - def patch_broker(self, broker: NatsBroker) -> TestNatsBroker: - return TestNatsBroker(broker) - - def get_fake_producer_class(self) -> type: - return FakeProducer - - @pytest.mark.asyncio +@pytest.mark.asyncio() +class TestTestclient(NatsMemoryTestcaseConfig, BrokerTestclientTestcase): + @pytest.mark.asyncio() async def test_stream_publish( self, queue: str, - ): - pub_broker = NatsBroker(apply_types=False) + ) -> None: + pub_broker = self.get_broker(apply_types=False) @pub_broker.subscriber(queue, stream="test") - async def m(msg): ... + async def m(msg) -> None: ... - async with TestNatsBroker(pub_broker) as br: + async with self.patch_broker(pub_broker) as br: await br.publish("Hi!", queue, stream="test") m.mock.assert_called_once_with("Hi!") - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_wrong_stream_publish( self, queue: str, - ): - pub_broker = NatsBroker(apply_types=False) + ) -> None: + pub_broker = self.get_broker(apply_types=False) @pub_broker.subscriber(queue) - async def m(msg): ... + async def m(msg) -> None: ... - async with TestNatsBroker(pub_broker) as br: + async with self.patch_broker(pub_broker) as br: await br.publish("Hi!", queue, stream="test") assert not m.mock.called - @pytest.mark.asyncio - async def test_rpc_conflicts_reply(self, queue): - async with TestNatsBroker(NatsBroker()) as br: - with pytest.raises(SetupError): - await br.publish( - "", - queue, - rpc=True, - reply_to="response", - ) - - @pytest.mark.nats + @pytest.mark.nats() async def test_with_real_testclient( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + broker = self.get_broker() @broker.subscriber(queue) - def subscriber(m): + def subscriber(m) -> None: event.set() - async with TestNatsBroker(broker, with_real=True) as br: + async with self.patch_broker(broker, with_real=True) as br: await asyncio.wait( ( asyncio.create_task(br.publish("hello", queue)), @@ -84,18 +68,18 @@ def subscriber(m): assert event.is_set() - @pytest.mark.nats + @pytest.mark.nats() async def test_inbox_prefix_with_real( self, queue: str, - ): - broker = NatsBroker(inbox_prefix="test") + ) -> None: + broker = self.get_broker(inbox_prefix="test") - async with TestNatsBroker(broker, with_real=True) as br: + async with self.patch_broker(broker, with_real=True) as br: assert br._connection._inbox_prefix == b"test" assert "test" in str(br._connection.new_inbox()) - async def test_respect_middleware(self, queue): + async def test_respect_middleware(self, queue) -> None: routes = [] class Middleware(BaseMiddleware): @@ -103,22 +87,22 @@ async def on_receive(self) -> None: routes.append(None) return await super().on_receive() - broker = NatsBroker(middlewares=(Middleware,)) + broker = self.get_broker(middlewares=(Middleware,)) @broker.subscriber(queue) - async def h1(): ... + async def h1(m) -> None: ... @broker.subscriber(queue + "1") - async def h2(): ... + async def h2(m) -> None: ... - async with TestNatsBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("", queue) await br.publish("", queue + "1") assert len(routes) == 2 - @pytest.mark.nats - async def test_real_respect_middleware(self, queue): + @pytest.mark.nats() + async def test_real_respect_middleware(self, queue) -> None: routes = [] class Middleware(BaseMiddleware): @@ -126,15 +110,15 @@ async def on_receive(self) -> None: routes.append(None) return await super().on_receive() - broker = NatsBroker(middlewares=(Middleware,)) + broker = self.get_broker(middlewares=(Middleware,)) @broker.subscriber(queue) - async def h1(): ... + async def h1(m) -> None: ... @broker.subscriber(queue + "1") - async def h2(): ... + async def h2(m) -> None: ... - async with TestNatsBroker(broker, with_real=True) as br: + async with self.patch_broker(broker, with_real=True) as br: await br.publish("", queue) await br.publish("", queue + "1") await h1.wait_call(3) @@ -146,14 +130,14 @@ async def test_js_subscriber_mock( self, queue: str, stream: JStream, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(queue, stream=stream) - async def m(msg): + async def m(msg) -> None: pass - async with TestNatsBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("hello", queue, stream=stream.name) m.mock.assert_called_once_with("hello") @@ -161,47 +145,47 @@ async def test_js_publisher_mock( self, queue: str, stream: JStream, - ): + ) -> None: broker = self.get_broker() publisher = broker.publisher(queue + "resp") @publisher @broker.subscriber(queue, stream=stream) - async def m(msg): + async def m(msg) -> str: return "response" - async with TestNatsBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("hello", queue, stream=stream.name) publisher.mock.assert_called_with("response") - async def test_any_subject_routing(self): + async def test_any_subject_routing(self) -> None: broker = self.get_broker() @broker.subscriber("test.*.subj.*") - def subscriber(msg): ... + def subscriber(msg) -> None: ... - async with TestNatsBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("hello", "test.a.subj.b") subscriber.mock.assert_called_once_with("hello") - async def test_ending_subject_routing(self): + async def test_ending_subject_routing(self) -> None: broker = self.get_broker() @broker.subscriber("test.>") - def subscriber(msg): ... + def subscriber(msg) -> None: ... - async with TestNatsBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("hello", "test.a.subj.b") subscriber.mock.assert_called_once_with("hello") - async def test_mixed_subject_routing(self): + async def test_mixed_subject_routing(self) -> None: broker = self.get_broker() @broker.subscriber("*.*.subj.>") - def subscriber(msg): ... + def subscriber(msg) -> None: ... - async with TestNatsBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("hello", "test.a.subj.b.c") subscriber.mock.assert_called_once_with("hello") @@ -209,13 +193,13 @@ async def test_consume_pull( self, queue: str, stream: JStream, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(queue, stream=stream, pull_sub=PullSub(1)) - def subscriber(m): ... + def subscriber(m) -> None: ... - async with TestNatsBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("hello", queue) subscriber.mock.assert_called_once_with("hello") @@ -223,7 +207,7 @@ async def test_consume_batch( self, queue: str, stream: JStream, - ): + ) -> None: broker = self.get_broker() @broker.subscriber( @@ -231,41 +215,42 @@ async def test_consume_batch( stream=stream, pull_sub=PullSub(1, batch=True), ) - def subscriber(m): + def subscriber(m) -> None: pass - async with TestNatsBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("hello", queue) subscriber.mock.assert_called_once_with(["hello"]) async def test_consume_with_filter( self, queue, - ): + ) -> None: broker = self.get_broker() @broker.subscriber( config=ConsumerConfig(filter_subjects=[f"{queue}.a"]), stream=JStream(queue, subjects=[f"{queue}.*"]), ) - def subscriber(m): + def subscriber(m) -> None: pass - async with TestNatsBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish(1, f"{queue}.b") await br.publish(2, f"{queue}.a") subscriber.mock.assert_called_once_with(2) - @pytest.mark.nats - async def test_broker_gets_patched_attrs_within_cm(self): - await super().test_broker_gets_patched_attrs_within_cm() + @pytest.mark.nats() + async def test_broker_gets_patched_attrs_within_cm(self) -> None: + await super().test_broker_gets_patched_attrs_within_cm(FakeProducer) - @pytest.mark.nats - async def test_broker_with_real_doesnt_get_patched(self): + @pytest.mark.nats() + async def test_broker_with_real_doesnt_get_patched(self) -> None: await super().test_broker_with_real_doesnt_get_patched() - @pytest.mark.nats + @pytest.mark.nats() async def test_broker_with_real_patches_publishers_and_subscribers( - self, queue: str - ): + self, + queue: str, + ) -> None: await super().test_broker_with_real_patches_publishers_and_subscribers(queue) diff --git a/tests/brokers/rabbit/basic.py b/tests/brokers/rabbit/basic.py new file mode 100644 index 0000000000..6a451530c5 --- /dev/null +++ b/tests/brokers/rabbit/basic.py @@ -0,0 +1,24 @@ +from typing import Any + +from faststream.rabbit import RabbitBroker, RabbitRouter, TestRabbitBroker +from tests.brokers.base.basic import BaseTestcaseConfig + + +class RabbitTestcaseConfig(BaseTestcaseConfig): + def get_broker( + self, + apply_types: bool = False, + **kwargs: Any, + ) -> RabbitBroker: + return RabbitBroker(apply_types=apply_types, **kwargs) + + def patch_broker(self, broker: RabbitBroker, **kwargs: Any) -> RabbitBroker: + return broker + + def get_router(self, **kwargs: Any) -> RabbitRouter: + return RabbitRouter(**kwargs) + + +class RabbitMemoryTestcaseConfig(RabbitTestcaseConfig): + def patch_broker(self, broker: RabbitBroker, **kwargs: Any) -> RabbitBroker: + return TestRabbitBroker(broker, **kwargs) diff --git a/tests/brokers/rabbit/conftest.py b/tests/brokers/rabbit/conftest.py index 00ff9bcde3..f36b591894 100644 --- a/tests/brokers/rabbit/conftest.py +++ b/tests/brokers/rabbit/conftest.py @@ -1,29 +1,26 @@ from dataclasses import dataclass import pytest -import pytest_asyncio from faststream.rabbit import ( - RabbitBroker, RabbitExchange, RabbitRouter, - TestRabbitBroker, ) @dataclass class Settings: - url = "amqp://guest:guest@localhost:5672/" # pragma: allowlist secret + url: str = "amqp://guest:guest@localhost:5672/" # pragma: allowlist secret - host = "localhost" - port = 5672 - login = "guest" - password = "guest" # pragma: allowlist secret + host: str = "localhost" + port: int = 5672 + login: str = "guest" + password: str = "guest" # pragma: allowlist secret - queue = "test_queue" + queue: str = "test_queue" -@pytest.fixture +@pytest.fixture() def exchange(queue): return RabbitExchange(name=queue) @@ -33,27 +30,6 @@ def settings(): return Settings() -@pytest.fixture +@pytest.fixture() def router(): return RabbitRouter() - - -@pytest_asyncio.fixture() -async def broker(settings): - broker = RabbitBroker(settings.url, apply_types=False) - async with broker: - yield broker - - -@pytest_asyncio.fixture() -async def full_broker(settings): - broker = RabbitBroker(settings.url) - async with broker: - yield broker - - -@pytest_asyncio.fixture() -async def test_broker(): - broker = RabbitBroker() - async with TestRabbitBroker(broker) as br: - yield br diff --git a/tests/brokers/rabbit/core/test_call_manual.py b/tests/brokers/rabbit/core/test_call_manual.py index 756113534f..07bf62c3c6 100644 --- a/tests/brokers/rabbit/core/test_call_manual.py +++ b/tests/brokers/rabbit/core/test_call_manual.py @@ -8,8 +8,8 @@ def just_broker(request): return request.param -@pytest.mark.asyncio # run it async to create anyio.Event -async def test_sync(just_broker: RabbitBroker): +@pytest.mark.asyncio() # run it async to create anyio.Event +async def test_sync(just_broker: RabbitBroker) -> None: @just_broker.subscriber("test") def func(a: int) -> str: return "pong" @@ -17,8 +17,8 @@ def func(a: int) -> str: assert func(1) == "pong" -@pytest.mark.asyncio # run it async to create anyio.Event -async def test_sync_publisher(just_broker: RabbitBroker): +@pytest.mark.asyncio() # run it async to create anyio.Event +async def test_sync_publisher(just_broker: RabbitBroker) -> None: @just_broker.publisher("test") def func(a: int) -> str: return "pong" @@ -26,8 +26,8 @@ def func(a: int) -> str: assert func(1) == "pong" -@pytest.mark.asyncio # run it async to create anyio.Event -async def test_sync_multi(just_broker: RabbitBroker): +@pytest.mark.asyncio() # run it async to create anyio.Event +async def test_sync_multi(just_broker: RabbitBroker) -> None: @just_broker.publisher("test") @just_broker.subscriber("test") @just_broker.publisher("test") @@ -37,8 +37,8 @@ def func(a: int) -> str: assert func(1) == "pong" -@pytest.mark.asyncio -async def test_async(just_broker: RabbitBroker): +@pytest.mark.asyncio() +async def test_async(just_broker: RabbitBroker) -> None: @just_broker.subscriber("test") async def func(a: int) -> str: return "pong" @@ -46,8 +46,8 @@ async def func(a: int) -> str: assert await func(1) == "pong" -@pytest.mark.asyncio -async def test_async_publisher(just_broker: RabbitBroker): +@pytest.mark.asyncio() +async def test_async_publisher(just_broker: RabbitBroker) -> None: @just_broker.publisher("test") async def func(a: int) -> str: return "pong" @@ -55,8 +55,8 @@ async def func(a: int) -> str: assert await func(1) == "pong" -@pytest.mark.asyncio -async def test_async_multi(just_broker: RabbitBroker): +@pytest.mark.asyncio() +async def test_async_multi(just_broker: RabbitBroker) -> None: @just_broker.publisher("test") @just_broker.subscriber("test") @just_broker.publisher("test") diff --git a/tests/brokers/rabbit/core/test_depends.py b/tests/brokers/rabbit/core/test_depends.py index 7b4f0e72bd..7b50cef000 100644 --- a/tests/brokers/rabbit/core/test_depends.py +++ b/tests/brokers/rabbit/core/test_depends.py @@ -1,17 +1,16 @@ import aio_pika import pytest +from faststream import Depends from faststream.rabbit import RabbitBroker from faststream.rabbit.annotations import RabbitMessage -from faststream.utils import Depends -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_broker_depends( - queue, - full_broker: RabbitBroker, -): +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_broker_depends(queue: str) -> None: + full_broker = RabbitBroker(apply_types=True) + def sync_depends(message: RabbitMessage): return message @@ -25,7 +24,7 @@ async def h( message: RabbitMessage, k1=Depends(sync_depends), k2=Depends(async_depends), - ): + ) -> None: nonlocal check_message check_message = ( isinstance(message.raw_message, aio_pika.IncomingMessage) @@ -35,34 +34,35 @@ async def h( await full_broker.start() - await full_broker.publish(queue=queue, rpc=True) + await full_broker.request(queue=queue) assert check_message is True -@pytest.mark.asyncio -@pytest.mark.rabbit +@pytest.mark.asyncio() +@pytest.mark.rabbit() async def test_different_consumers_has_different_messages( context, - full_broker: RabbitBroker, -): +) -> None: + full_broker = RabbitBroker(apply_types=True) + message1 = None @full_broker.subscriber("test_different_consume_1") - async def consumer1(message: RabbitMessage): + async def consumer1(message: RabbitMessage) -> None: nonlocal message1 message1 = message message2 = None @full_broker.subscriber("test_different_consume_2") - async def consumer2(message: RabbitMessage): + async def consumer2(message: RabbitMessage) -> None: nonlocal message2 message2 = message await full_broker.start() - await full_broker.publish(queue="test_different_consume_1", rpc=True) - await full_broker.publish(queue="test_different_consume_2", rpc=True) + await full_broker.request(queue="test_different_consume_1") + await full_broker.request(queue="test_different_consume_2") assert isinstance(message1.raw_message, aio_pika.IncomingMessage) assert isinstance(message2.raw_message, aio_pika.IncomingMessage) diff --git a/tests/brokers/rabbit/future/test_fastapi.py b/tests/brokers/rabbit/future/test_fastapi.py index fa556342ef..f02da3f8fb 100644 --- a/tests/brokers/rabbit/future/test_fastapi.py +++ b/tests/brokers/rabbit/future/test_fastapi.py @@ -1,11 +1,11 @@ import pytest +from faststream.rabbit.broker import RabbitRouter from faststream.rabbit.fastapi import RabbitRouter as StreamRouter -from faststream.rabbit.router import RabbitRouter from tests.brokers.base.future.fastapi import FastapiTestCase -@pytest.mark.rabbit +@pytest.mark.rabbit() class TestRouter(FastapiTestCase): router_class = StreamRouter broker_router_class = RabbitRouter diff --git a/tests/brokers/rabbit/specific/test_channels.py b/tests/brokers/rabbit/specific/test_channels.py index faad6f4e45..896e70fd1b 100644 --- a/tests/brokers/rabbit/specific/test_channels.py +++ b/tests/brokers/rabbit/specific/test_channels.py @@ -5,9 +5,9 @@ from faststream.rabbit import Channel, RabbitBroker -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_subscriber_use_shared_channel(): +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_subscriber_use_shared_channel() -> None: broker = RabbitBroker(logger=None) sub1 = broker.subscriber(uuid4().hex) diff --git a/tests/brokers/rabbit/specific/test_declare.py b/tests/brokers/rabbit/specific/test_declare.py index 4b8b3a44ee..8919f685aa 100644 --- a/tests/brokers/rabbit/specific/test_declare.py +++ b/tests/brokers/rabbit/specific/test_declare.py @@ -1,9 +1,10 @@ from typing import TYPE_CHECKING, Optional +from unittest.mock import AsyncMock import pytest from faststream.rabbit import RabbitBroker, RabbitExchange, RabbitQueue -from faststream.rabbit.helpers.declarer import RabbitDeclarer +from faststream.rabbit.helpers.declarer import RabbitDeclarerImpl if TYPE_CHECKING: import aio_pika @@ -12,7 +13,7 @@ class FakeChannelManager: - def __init__(self, async_mock): + def __init__(self, async_mock: AsyncMock) -> None: self.async_mock = async_mock async def get_channel( @@ -22,9 +23,9 @@ async def get_channel( return self.async_mock -@pytest.mark.asyncio -async def test_declare_queue(async_mock, queue: str) -> None: - declarer = RabbitDeclarer(FakeChannelManager(async_mock)) +@pytest.mark.asyncio() +async def test_declare_queue(async_mock: AsyncMock, queue: str) -> None: + declarer = RabbitDeclarerImpl(FakeChannelManager(async_mock)) q1 = await declarer.declare_queue(RabbitQueue(queue)) q2 = await declarer.declare_queue(RabbitQueue(queue)) @@ -33,9 +34,9 @@ async def test_declare_queue(async_mock, queue: str) -> None: async_mock.declare_queue.assert_awaited_once() -@pytest.mark.asyncio -async def test_declare_exchange(async_mock, queue: str) -> None: - declarer = RabbitDeclarer(FakeChannelManager(async_mock)) +@pytest.mark.asyncio() +async def test_declare_exchange(async_mock: AsyncMock, queue: str) -> None: + declarer = RabbitDeclarerImpl(FakeChannelManager(async_mock)) ex1 = await declarer.declare_exchange(RabbitExchange(queue)) ex2 = await declarer.declare_exchange(RabbitExchange(queue)) @@ -44,9 +45,11 @@ async def test_declare_exchange(async_mock, queue: str) -> None: async_mock.declare_exchange.assert_awaited_once() -@pytest.mark.asyncio -async def test_declare_nested_exchange_cash_nested(async_mock, queue: str) -> None: - declarer = RabbitDeclarer(FakeChannelManager(async_mock)) +@pytest.mark.asyncio() +async def test_declare_nested_exchange_cash_nested( + async_mock: AsyncMock, queue: str +) -> None: + declarer = RabbitDeclarerImpl(FakeChannelManager(async_mock)) exchange = RabbitExchange(queue) @@ -57,18 +60,18 @@ async def test_declare_nested_exchange_cash_nested(async_mock, queue: str) -> No assert async_mock.declare_exchange.await_count == 2 -@pytest.mark.asyncio -async def test_publisher_declare(async_mock, queue: str) -> None: - declarer = RabbitDeclarer(FakeChannelManager(async_mock)) +@pytest.mark.asyncio() +async def test_publisher_declare(async_mock: AsyncMock, queue: str) -> None: + declarer = RabbitDeclarerImpl(FakeChannelManager(async_mock)) broker = RabbitBroker() broker._connection = async_mock - broker.declarer = declarer + broker.config.declarer = declarer @broker.publisher(queue, queue) - async def f(): ... + async def f() -> None: ... await broker.start() - assert not async_mock.declare_queue.await_count - async_mock.declare_exchange.assert_awaited_once() + assert RabbitQueue.validate(queue) not in declarer._queues + assert RabbitExchange.validate(queue) in declarer._exchanges diff --git a/tests/brokers/rabbit/specific/test_init.py b/tests/brokers/rabbit/specific/test_init.py index 127fbc2e57..f8a1ed872b 100644 --- a/tests/brokers/rabbit/specific/test_init.py +++ b/tests/brokers/rabbit/specific/test_init.py @@ -3,8 +3,8 @@ from faststream.rabbit import Channel, RabbitBroker -@pytest.mark.asyncio -@pytest.mark.rabbit +@pytest.mark.asyncio() +@pytest.mark.rabbit() async def test_set_max(): broker = RabbitBroker( logger=None, diff --git a/tests/brokers/rabbit/specific/test_nested_exchange.py b/tests/brokers/rabbit/specific/test_nested_exchange.py index d0fbe5a031..821091cea3 100644 --- a/tests/brokers/rabbit/specific/test_nested_exchange.py +++ b/tests/brokers/rabbit/specific/test_nested_exchange.py @@ -2,24 +2,29 @@ import pytest -from faststream.rabbit import ExchangeType, RabbitBroker, RabbitExchange, RabbitQueue +from faststream.rabbit import ExchangeType, RabbitBroker, RabbitExchange -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_bind_to(queue: RabbitQueue, broker: RabbitBroker): +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_bind_to(queue: str) -> None: + broker = RabbitBroker(apply_types=False) + consume = Event() async with broker: meta_parent = RabbitExchange("meta", type=ExchangeType.FANOUT) parent_exch = RabbitExchange( - "main", type=ExchangeType.FANOUT, bind_to=meta_parent + "main", + type=ExchangeType.FANOUT, + bind_to=meta_parent, ) @broker.subscriber( - queue, exchange=RabbitExchange("nested", bind_to=parent_exch) + queue, + exchange=RabbitExchange("nested", bind_to=parent_exch), ) - async def handler(m): + async def handler(m) -> None: consume.set() await broker.start() diff --git a/tests/brokers/rabbit/test_config.py b/tests/brokers/rabbit/test_config.py new file mode 100644 index 0000000000..a2a92bfcc5 --- /dev/null +++ b/tests/brokers/rabbit/test_config.py @@ -0,0 +1,49 @@ +from unittest.mock import MagicMock + +from faststream import AckPolicy +from faststream.rabbit.subscriber.config import RabbitSubscriberConfig + + +def test_default() -> None: + config = RabbitSubscriberConfig( + _outer_config=MagicMock(), + queue=MagicMock(), + exchange=MagicMock(), + ) + + assert config.ack_policy is AckPolicy.REJECT_ON_ERROR + + +def test_ack_first() -> None: + config = RabbitSubscriberConfig( + _outer_config=MagicMock(), + queue=MagicMock(), + exchange=MagicMock(), + _ack_policy=AckPolicy.ACK_FIRST, + ) + + assert config.ack_policy is AckPolicy.DO_NOTHING + assert config.ack_first + + +def test_custom_ack() -> None: + config = RabbitSubscriberConfig( + _outer_config=MagicMock(), + queue=MagicMock(), + exchange=MagicMock(), + _ack_policy=AckPolicy.ACK, + ) + + assert config.ack_policy is AckPolicy.ACK + + +def test_no_ack() -> None: + config = RabbitSubscriberConfig( + _outer_config=MagicMock(), + queue=MagicMock(), + exchange=MagicMock(), + _no_ack=True, + ) + + assert config.ack_policy is AckPolicy.DO_NOTHING + assert not config.ack_first diff --git a/tests/brokers/rabbit/test_connect.py b/tests/brokers/rabbit/test_connect.py index 61934ec043..bd5fd16ca8 100644 --- a/tests/brokers/rabbit/test_connect.py +++ b/tests/brokers/rabbit/test_connect.py @@ -1,5 +1,3 @@ -from typing import Type - import pytest from faststream.rabbit import RabbitBroker @@ -7,15 +5,17 @@ from tests.brokers.base.connection import BrokerConnectionTestcase -@pytest.mark.rabbit +@pytest.mark.rabbit() class TestConnection(BrokerConnectionTestcase): - broker: Type[RabbitBroker] = RabbitBroker + broker: type[RabbitBroker] = RabbitBroker def get_broker_args(self, settings): return {"url": settings.url} - @pytest.mark.asyncio - async def test_connect_handover_config_to_init(self, settings): + @pytest.mark.asyncio() + async def test_connect_handover_config_to_init( + self, settings: dict[str, str] + ) -> None: broker = self.broker( host=settings.host, port=settings.port, @@ -26,22 +26,3 @@ async def test_connect_handover_config_to_init(self, settings): ) assert await broker.connect() await broker.close() - - @pytest.mark.asyncio - async def test_connect_handover_config_to_connect(self, settings): - broker = self.broker() - assert await broker.connect( - host=settings.host, - port=settings.port, - security=SASLPlaintext( - username=settings.login, - password=settings.password, - ), - ) - await broker.close() - - @pytest.mark.asyncio - async def test_connect_handover_config_to_connect_override_init(self, settings): - broker = self.broker("fake-url") # will be ignored - assert await broker.connect(url=settings.url) - await broker.close() diff --git a/tests/brokers/rabbit/test_consume.py b/tests/brokers/rabbit/test_consume.py index a85c333e9b..3f2c2eba48 100644 --- a/tests/brokers/rabbit/test_consume.py +++ b/tests/brokers/rabbit/test_consume.py @@ -3,66 +3,67 @@ import pytest from aio_pika import IncomingMessage, Message +from aiormq.abc import ConfirmationFrameType +from faststream import AckPolicy from faststream.exceptions import AckMessage, NackMessage, RejectMessage, SkipMessage -from faststream.rabbit import RabbitBroker, RabbitExchange, RabbitQueue +from faststream.rabbit import RabbitExchange, RabbitQueue from faststream.rabbit.annotations import RabbitMessage from tests.brokers.base.consume import BrokerRealConsumeTestcase from tests.tools import spy_decorator +from .basic import RabbitTestcaseConfig -@pytest.mark.rabbit -class TestConsume(BrokerRealConsumeTestcase): - def get_broker(self, apply_types: bool = False) -> RabbitBroker: - return RabbitBroker(apply_types=apply_types) - @pytest.mark.asyncio +@pytest.mark.rabbit() +class TestConsume(RabbitTestcaseConfig, BrokerRealConsumeTestcase): + @pytest.mark.asyncio() async def test_consume_from_exchange( self, queue: str, exchange: RabbitExchange, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() - @consume_broker.subscriber(queue=queue, exchange=exchange, retry=1) - def h(m): + @consume_broker.subscriber(queue=queue, exchange=exchange) + def h(m) -> None: event.set() async with self.patch_broker(consume_broker) as br: await br.start() + + result = await br.publish("hello", queue=queue, exchange=exchange) await asyncio.wait( - ( - asyncio.create_task( - br.publish("hello", queue=queue, exchange=exchange) - ), - asyncio.create_task(event.wait()), - ), + (asyncio.create_task(event.wait()),), timeout=3, ) + assert isinstance(result, ConfirmationFrameType), result assert event.is_set() - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_with_get_old( self, queue: str, exchange: RabbitExchange, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber( queue=RabbitQueue(name=queue, declare=False), exchange=RabbitExchange(name=exchange.name, declare=False), - retry=True, ) - def h(m): + def h(m) -> None: event.set() - async with self.patch_broker(consume_broker) as br: - await br.declare_queue(RabbitQueue(queue)) - await br.declare_exchange(exchange) + async with self.patch_broker(consume_broker, connect_only=True) as br: + q = await br.declare_queue(RabbitQueue(queue)) + ex = await br.declare_exchange(exchange) + await q.bind(ex, routing_key=queue) await br.start() @@ -73,7 +74,7 @@ def h(m): Message(b"hello"), queue=queue, exchange=exchange.name, - ) + ), ), asyncio.create_task(event.wait()), ), @@ -82,29 +83,32 @@ def h(m): assert event.is_set() - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_ack( self, queue: str, exchange: RabbitExchange, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue=queue, exchange=exchange, retry=1) - async def handler(msg: RabbitMessage): + @consume_broker.subscriber(queue=queue, exchange=exchange) + async def handler(msg: RabbitMessage) -> None: event.set() async with self.patch_broker(consume_broker) as br: await br.start() with patch.object( - IncomingMessage, "ack", spy_decorator(IncomingMessage.ack) + IncomingMessage, + "ack", + spy_decorator(IncomingMessage.ack), ) as m: await asyncio.wait( ( asyncio.create_task( - br.publish("hello", queue=queue, exchange=exchange) + br.publish("hello", queue=queue, exchange=exchange), ), asyncio.create_task(event.wait()), ), @@ -114,17 +118,18 @@ async def handler(msg: RabbitMessage): assert event.is_set() - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_manual_ack( self, queue: str, exchange: RabbitExchange, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue=queue, exchange=exchange, retry=1) - async def handler(msg: RabbitMessage): + @consume_broker.subscriber(queue=queue, exchange=exchange) + async def handler(msg: RabbitMessage) -> None: await msg.ack() event.set() @@ -132,12 +137,14 @@ async def handler(msg: RabbitMessage): await br.start() with patch.object( - IncomingMessage, "ack", spy_decorator(IncomingMessage.ack) + IncomingMessage, + "ack", + spy_decorator(IncomingMessage.ack), ) as m: await asyncio.wait( ( asyncio.create_task( - br.publish("hello", queue=queue, exchange=exchange) + br.publish("hello", queue=queue, exchange=exchange), ), asyncio.create_task(event.wait()), ), @@ -146,19 +153,20 @@ async def handler(msg: RabbitMessage): m.mock.assert_called_once() assert event.is_set() - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_exception_ack( self, queue: str, exchange: RabbitExchange, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue=queue, exchange=exchange, retry=1) - async def handler(msg: RabbitMessage): + @consume_broker.subscriber(queue=queue, exchange=exchange) + async def handler(msg: RabbitMessage) -> None: try: - raise AckMessage() + raise AckMessage finally: event.set() @@ -166,12 +174,14 @@ async def handler(msg: RabbitMessage): await br.start() with patch.object( - IncomingMessage, "ack", spy_decorator(IncomingMessage.ack) + IncomingMessage, + "ack", + spy_decorator(IncomingMessage.ack), ) as m: await asyncio.wait( ( asyncio.create_task( - br.publish("hello", queue=queue, exchange=exchange) + br.publish("hello", queue=queue, exchange=exchange), ), asyncio.create_task(event.wait()), ), @@ -180,31 +190,34 @@ async def handler(msg: RabbitMessage): m.mock.assert_called_once() assert event.is_set() - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_manual_nack( self, queue: str, exchange: RabbitExchange, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue=queue, exchange=exchange, retry=1) + @consume_broker.subscriber(queue=queue, exchange=exchange) async def handler(msg: RabbitMessage): await msg.nack() event.set() - raise ValueError() + raise ValueError async with self.patch_broker(consume_broker) as br: await br.start() with patch.object( - IncomingMessage, "nack", spy_decorator(IncomingMessage.nack) + IncomingMessage, + "nack", + spy_decorator(IncomingMessage.nack), ) as m: await asyncio.wait( ( asyncio.create_task( - br.publish("hello", queue=queue, exchange=exchange) + br.publish("hello", queue=queue, exchange=exchange), ), asyncio.create_task(event.wait()), ), @@ -213,19 +226,20 @@ async def handler(msg: RabbitMessage): m.mock.assert_called_once() assert event.is_set() - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_exception_nack( self, queue: str, exchange: RabbitExchange, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue=queue, exchange=exchange, retry=1) - async def handler(msg: RabbitMessage): + @consume_broker.subscriber(queue=queue, exchange=exchange) + async def handler(msg: RabbitMessage) -> None: try: - raise NackMessage() + raise NackMessage finally: event.set() @@ -233,12 +247,14 @@ async def handler(msg: RabbitMessage): await br.start() with patch.object( - IncomingMessage, "nack", spy_decorator(IncomingMessage.nack) + IncomingMessage, + "nack", + spy_decorator(IncomingMessage.nack), ) as m: await asyncio.wait( ( asyncio.create_task( - br.publish("hello", queue=queue, exchange=exchange) + br.publish("hello", queue=queue, exchange=exchange), ), asyncio.create_task(event.wait()), ), @@ -247,31 +263,34 @@ async def handler(msg: RabbitMessage): m.mock.assert_called_once() assert event.is_set() - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_manual_reject( self, queue: str, exchange: RabbitExchange, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue=queue, exchange=exchange, retry=1) + @consume_broker.subscriber(queue=queue, exchange=exchange) async def handler(msg: RabbitMessage): await msg.reject() event.set() - raise ValueError() + raise ValueError async with self.patch_broker(consume_broker) as br: await br.start() with patch.object( - IncomingMessage, "reject", spy_decorator(IncomingMessage.reject) + IncomingMessage, + "reject", + spy_decorator(IncomingMessage.reject), ) as m: await asyncio.wait( ( asyncio.create_task( - br.publish("hello", queue=queue, exchange=exchange) + br.publish("hello", queue=queue, exchange=exchange), ), asyncio.create_task(event.wait()), ), @@ -280,19 +299,20 @@ async def handler(msg: RabbitMessage): m.mock.assert_called_once() assert event.is_set() - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_exception_reject( self, queue: str, exchange: RabbitExchange, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue=queue, exchange=exchange, retry=1) - async def handler(msg: RabbitMessage): + @consume_broker.subscriber(queue=queue, exchange=exchange) + async def handler(msg: RabbitMessage) -> None: try: - raise RejectMessage() + raise RejectMessage finally: event.set() @@ -300,12 +320,14 @@ async def handler(msg: RabbitMessage): await br.start() with patch.object( - IncomingMessage, "reject", spy_decorator(IncomingMessage.reject) + IncomingMessage, + "reject", + spy_decorator(IncomingMessage.reject), ) as m: await asyncio.wait( ( asyncio.create_task( - br.publish("hello", queue=queue, exchange=exchange) + br.publish("hello", queue=queue, exchange=exchange), ), asyncio.create_task(event.wait()), ), @@ -314,31 +336,42 @@ async def handler(msg: RabbitMessage): m.mock.assert_called_once() assert event.is_set() - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_skip_message( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber(queue) - async def handler(msg: RabbitMessage): + async def handler(msg: RabbitMessage) -> None: try: - raise SkipMessage() + raise SkipMessage finally: event.set() async with self.patch_broker(consume_broker) as br: await br.start() - with patch.object( - IncomingMessage, "reject", spy_decorator(IncomingMessage.reject) - ) as m, patch.object( - IncomingMessage, "reject", spy_decorator(IncomingMessage.reject) - ) as m1, patch.object( - IncomingMessage, "reject", spy_decorator(IncomingMessage.reject) - ) as m2: + with ( + patch.object( + IncomingMessage, + "reject", + spy_decorator(IncomingMessage.reject), + ) as m, + patch.object( + IncomingMessage, + "reject", + spy_decorator(IncomingMessage.reject), + ) as m1, + patch.object( + IncomingMessage, + "reject", + spy_decorator(IncomingMessage.reject), + ) as m2, + ): await asyncio.wait( ( asyncio.create_task(br.publish("hello", queue)), @@ -352,29 +385,36 @@ async def handler(msg: RabbitMessage): assert event.is_set() - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_consume_no_ack( self, queue: str, exchange: RabbitExchange, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) - @consume_broker.subscriber(queue, exchange=exchange, retry=1, no_ack=True) - async def handler(msg: RabbitMessage): + @consume_broker.subscriber( + queue, + exchange=exchange, + ack_policy=AckPolicy.DO_NOTHING, + ) + async def handler(msg: RabbitMessage) -> None: event.set() async with self.patch_broker(consume_broker) as br: await br.start() with patch.object( - IncomingMessage, "ack", spy_decorator(IncomingMessage.ack) + IncomingMessage, + "ack", + spy_decorator(IncomingMessage.ack), ) as m: await asyncio.wait( ( asyncio.create_task( - br.publish("hello", queue=queue, exchange=exchange) + br.publish("hello", queue=queue, exchange=exchange), ), asyncio.create_task(event.wait()), ), diff --git a/tests/brokers/rabbit/test_fastapi.py b/tests/brokers/rabbit/test_fastapi.py index 18fff70dc6..6ffa26392a 100644 --- a/tests/brokers/rabbit/test_fastapi.py +++ b/tests/brokers/rabbit/test_fastapi.py @@ -5,22 +5,24 @@ from faststream.rabbit import ExchangeType, RabbitExchange, RabbitQueue, RabbitRouter from faststream.rabbit.fastapi import RabbitRouter as StreamRouter -from faststream.rabbit.testing import TestRabbitBroker, build_message from tests.brokers.base.fastapi import FastAPILocalTestcase, FastAPITestcase +from .basic import RabbitMemoryTestcaseConfig -@pytest.mark.rabbit + +@pytest.mark.rabbit() class TestRouter(FastAPITestcase): router_class = StreamRouter broker_router_class = RabbitRouter - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_path( self, queue: str, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() @router.subscriber( @@ -33,7 +35,7 @@ async def test_path( type=ExchangeType.TOPIC, ), ) - def subscriber(msg: str, name: str): + def subscriber(msg: str, name: str) -> None: mock(msg=msg, name=name) event.set() @@ -42,7 +44,7 @@ def subscriber(msg: str, name: str): await asyncio.wait( ( asyncio.create_task( - router.broker.publish("hello", "in.john", queue + "1") + router.broker.publish("hello", "in.john", queue + "1"), ), asyncio.create_task(event.wait()), ), @@ -53,14 +55,12 @@ def subscriber(msg: str, name: str): mock.assert_called_once_with(msg="hello", name="john") -@pytest.mark.asyncio -class TestRouterLocal(FastAPILocalTestcase): +@pytest.mark.asyncio() +class TestRouterLocal(RabbitMemoryTestcaseConfig, FastAPILocalTestcase): router_class = StreamRouter broker_router_class = RabbitRouter - broker_test = staticmethod(TestRabbitBroker) - build_message = staticmethod(build_message) - async def test_path(self): + async def test_path(self) -> None: router = self.router_class() @router.subscriber( @@ -76,12 +76,11 @@ async def test_path(self): async def hello(name): return name - async with self.broker_test(router.broker): - r = await router.broker.publish( + async with self.patch_broker(router.broker) as br: + r = await br.request( "hi", "in.john", "test", - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "john" + assert await r.decode() == "john" diff --git a/tests/brokers/rabbit/test_include_router.py b/tests/brokers/rabbit/test_include_router.py new file mode 100644 index 0000000000..e56604146a --- /dev/null +++ b/tests/brokers/rabbit/test_include_router.py @@ -0,0 +1,14 @@ +from tests.brokers.base.include_router import ( + IncludePublisherTestcase, + IncludeSubscriberTestcase, +) + +from .basic import RabbitTestcaseConfig + + +class TestSubscriber(RabbitTestcaseConfig, IncludeSubscriberTestcase): + pass + + +class TestPublisher(RabbitTestcaseConfig, IncludePublisherTestcase): + pass diff --git a/tests/brokers/rabbit/test_middlewares.py b/tests/brokers/rabbit/test_middlewares.py index 050d98d173..f56c836e8d 100644 --- a/tests/brokers/rabbit/test_middlewares.py +++ b/tests/brokers/rabbit/test_middlewares.py @@ -1,25 +1,23 @@ import pytest -from faststream.rabbit import RabbitBroker, TestRabbitBroker from tests.brokers.base.middlewares import ( ExceptionMiddlewareTestcase, MiddlewareTestcase, MiddlewaresOrderTestcase, ) +from .basic import RabbitMemoryTestcaseConfig, RabbitTestcaseConfig -@pytest.mark.rabbit -class TestMiddlewares(MiddlewareTestcase): - broker_class = RabbitBroker +class TestMiddlewaresOrder(RabbitMemoryTestcaseConfig, MiddlewaresOrderTestcase): + pass -@pytest.mark.rabbit -class TestExceptionMiddlewares(ExceptionMiddlewareTestcase): - broker_class = RabbitBroker +@pytest.mark.rabbit() +class TestMiddlewares(RabbitTestcaseConfig, MiddlewareTestcase): + pass -class TestMiddlewaresOrder(MiddlewaresOrderTestcase): - broker_class = RabbitBroker - def patch_broker(self, broker: RabbitBroker) -> TestRabbitBroker: - return TestRabbitBroker(broker) +@pytest.mark.rabbit() +class TestExceptionMiddlewares(RabbitTestcaseConfig, ExceptionMiddlewareTestcase): + pass diff --git a/tests/brokers/rabbit/test_misconfigure.py b/tests/brokers/rabbit/test_misconfigure.py index 3091bd5de8..1730bc16f1 100644 --- a/tests/brokers/rabbit/test_misconfigure.py +++ b/tests/brokers/rabbit/test_misconfigure.py @@ -1,7 +1,6 @@ import pytest from faststream.exceptions import SetupError -from faststream.kafka import KafkaRouter from faststream.nats import NatsRouter from faststream.rabbit import RabbitBroker, RabbitRouter @@ -13,7 +12,7 @@ def test_use_only_rabbit_router() -> None: with pytest.raises(SetupError): broker.include_router(router) - routers = [RabbitRouter(), NatsRouter(), KafkaRouter()] + routers = [RabbitRouter(), NatsRouter()] with pytest.raises(SetupError): broker.include_routers(routers) diff --git a/tests/brokers/rabbit/test_parser.py b/tests/brokers/rabbit/test_parser.py index 038f43ac93..2ecaf2967c 100644 --- a/tests/brokers/rabbit/test_parser.py +++ b/tests/brokers/rabbit/test_parser.py @@ -1,9 +1,10 @@ import pytest -from faststream.rabbit import RabbitBroker from tests.brokers.base.parser import CustomParserTestcase +from .basic import RabbitTestcaseConfig -@pytest.mark.rabbit -class TestCustomParser(CustomParserTestcase): - broker_class = RabbitBroker + +@pytest.mark.rabbit() +class TestCustomParser(RabbitTestcaseConfig, CustomParserTestcase): + pass diff --git a/tests/brokers/rabbit/test_publish.py b/tests/brokers/rabbit/test_publish.py index 719a0fd55e..43b9fc2b7c 100644 --- a/tests/brokers/rabbit/test_publish.py +++ b/tests/brokers/rabbit/test_publish.py @@ -1,95 +1,96 @@ import asyncio import datetime as dt -from unittest.mock import Mock, patch +from typing import TYPE_CHECKING +from unittest.mock import MagicMock, patch import pytest from dirty_equals import IsNow from faststream import Context -from faststream.rabbit import RabbitBroker, RabbitResponse, ReplyConfig -from faststream.rabbit.publisher.producer import AioPikaFastProducer +from faststream.rabbit import RabbitResponse +from faststream.rabbit.publisher.producer import AioPikaFastProducerImpl from tests.brokers.base.publish import BrokerPublishTestcase from tests.tools import spy_decorator +from .basic import RabbitTestcaseConfig -@pytest.mark.rabbit -class TestPublish(BrokerPublishTestcase): - def get_broker(self, apply_types: bool = False) -> RabbitBroker: - return RabbitBroker(apply_types=apply_types) +if TYPE_CHECKING: + from faststream.rabbit.response import RabbitPublishCommand - @pytest.mark.asyncio + +@pytest.mark.rabbit() +class TestPublish(RabbitTestcaseConfig, BrokerPublishTestcase): + @pytest.mark.asyncio() async def test_reply_config( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker() reply_queue = queue + "reply" @pub_broker.subscriber(reply_queue) - async def reply_handler(m): + async def reply_handler(m) -> None: event.set() mock(m) - with pytest.warns(DeprecationWarning): - - @pub_broker.subscriber(queue, reply_config=ReplyConfig(persist=True)) - async def handler(m): - return m + @pub_broker.subscriber(queue) + async def handler(m): + return RabbitResponse(m, persist=True) async with self.patch_broker(pub_broker) as br: with patch.object( - AioPikaFastProducer, + AioPikaFastProducerImpl, "publish", - spy_decorator(AioPikaFastProducer.publish), + spy_decorator(AioPikaFastProducerImpl.publish), ) as m: await br.start() await asyncio.wait( ( asyncio.create_task( - br.publish("Hello!", queue, reply_to=reply_queue) + br.publish("Hello!", queue, reply_to=reply_queue), ), asyncio.create_task(event.wait()), ), timeout=3, ) - assert m.mock.call_args.kwargs.get("persist") - assert m.mock.call_args.kwargs.get("immediate") is False + cmd: RabbitPublishCommand = m.mock.call_args[0][1] + assert cmd.message_options["persist"] + assert not cmd.publish_options["immediate"] assert event.is_set() mock.assert_called_with("Hello!") - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_response( self, queue: str, - event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) @pub_broker.subscriber(queue) @pub_broker.publisher(queue + "1") async def handle(): - return RabbitResponse( - 1, - persist=True, - ) + return RabbitResponse(1, persist=True) @pub_broker.subscriber(queue + "1") - async def handle_next(msg=Context("message")): + async def handle_next(msg=Context("message")) -> None: mock(body=msg.body) event.set() async with self.patch_broker(pub_broker) as br: with patch.object( - AioPikaFastProducer, + AioPikaFastProducerImpl, "publish", - spy_decorator(AioPikaFastProducer.publish), + spy_decorator(AioPikaFastProducerImpl.publish), ) as m: await br.start() @@ -103,16 +104,16 @@ async def handle_next(msg=Context("message")): assert event.is_set() - assert m.mock.call_args.kwargs.get("persist") + cmd: RabbitPublishCommand = m.mock.call_args[0][1] + assert cmd.message_options["persist"] mock.assert_called_once_with(body=b"1") - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_response_for_rpc( self, queue: str, - event: asyncio.Event, - ): + ) -> None: pub_broker = self.get_broker(apply_types=True) @pub_broker.subscriber(queue) @@ -123,19 +124,19 @@ async def handle(): await br.start() response = await asyncio.wait_for( - br.publish("", queue, rpc=True), + br.request("", queue), timeout=3, ) - assert response == "Hi!", response + assert await response.decode() == "Hi!", response - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_default_timestamp( self, queue: str, event: asyncio.Event, - mock: Mock, - ): + mock: MagicMock, + ) -> None: pub_broker = self.get_broker(apply_types=True) @pub_broker.subscriber(queue) @@ -156,6 +157,7 @@ async def handle(msg=Context("message")): assert event.is_set() - mock.assert_called_once_with( - body=b"", timestamp=IsNow(delta=3, tz=dt.timezone.utc) - ) + assert mock.call_args.kwargs == { + "body": b"", + "timestamp": IsNow(delta=dt.timedelta(seconds=10), tz=dt.timezone.utc), + } diff --git a/tests/brokers/rabbit/test_requests.py b/tests/brokers/rabbit/test_requests.py index c0927eabc8..8eb64a075a 100644 --- a/tests/brokers/rabbit/test_requests.py +++ b/tests/brokers/rabbit/test_requests.py @@ -1,38 +1,32 @@ import pytest from faststream import BaseMiddleware -from faststream.rabbit import RabbitBroker, RabbitRouter, TestRabbitBroker from tests.brokers.base.requests import RequestsTestcase +from .basic import RabbitMemoryTestcaseConfig, RabbitTestcaseConfig + class Mid(BaseMiddleware): async def on_receive(self) -> None: self.msg._Message__lock = False - self.msg.body = self.msg.body * 2 + self.msg.body *= 2 async def consume_scope(self, call_next, msg): - msg._decoded_body = msg._decoded_body * 2 + msg.body *= 2 return await call_next(msg) -@pytest.mark.asyncio +@pytest.mark.asyncio() class RabbitRequestsTestcase(RequestsTestcase): def get_middleware(self, **kwargs): return Mid - def get_broker(self, **kwargs): - return RabbitBroker(**kwargs) - - def get_router(self, **kwargs): - return RabbitRouter(**kwargs) - -@pytest.mark.rabbit -class TestRealRequests(RabbitRequestsTestcase): +@pytest.mark.rabbit() +class TestRealRequests(RabbitTestcaseConfig, RabbitRequestsTestcase): pass -@pytest.mark.asyncio -class TestRequestTestClient(RabbitRequestsTestcase): - def patch_broker(self, broker, **kwargs): - return TestRabbitBroker(broker, **kwargs) +@pytest.mark.asyncio() +class TestRequestTestClient(RabbitMemoryTestcaseConfig, RabbitRequestsTestcase): + pass diff --git a/tests/brokers/rabbit/test_router.py b/tests/brokers/rabbit/test_router.py index 6cbd2557a4..24c226be68 100644 --- a/tests/brokers/rabbit/test_router.py +++ b/tests/brokers/rabbit/test_router.py @@ -1,34 +1,34 @@ import asyncio +from unittest.mock import MagicMock import pytest from faststream import Path from faststream.rabbit import ( ExchangeType, - RabbitBroker, RabbitExchange, RabbitPublisher, RabbitQueue, RabbitRoute, - RabbitRouter, ) from tests.brokers.base.router import RouterLocalTestcase, RouterTestcase +from .basic import RabbitMemoryTestcaseConfig, RabbitTestcaseConfig -@pytest.mark.rabbit -class TestRouter(RouterTestcase): - broker_class = RabbitRouter + +@pytest.mark.rabbit() +class TestRouter(RabbitTestcaseConfig, RouterTestcase): route_class = RabbitRoute publisher_class = RabbitPublisher async def test_router_path( self, - queue, - event, - mock, - router, - pub_broker, - ): + queue: str, + event: asyncio.Event, + mock: MagicMock, + ) -> None: + router = self.get_router() + @router.subscriber( RabbitQueue( queue, @@ -42,20 +42,19 @@ async def test_router_path( async def h( name: str = Path(), id: int = Path("id"), - ): + ) -> None: event.set() mock(name=name, id=id) - pub_broker._is_apply_types = True + pub_broker = self.get_broker(apply_types=True) pub_broker.include_router(router) await pub_broker.start() - await pub_broker.publish( + await pub_broker.request( "", "in.john.2", queue + "1", - rpc=True, ) assert event.is_set() @@ -63,20 +62,18 @@ async def h( async def test_router_delay_handler_path( self, - queue, - event, - mock, - router, - pub_broker, - ): + queue: str, + event: asyncio.Event, + mock: MagicMock, + ) -> None: async def h( name: str = Path(), id: int = Path("id"), - ): + ) -> None: event.set() mock(name=name, id=id) - r = type(router)( + router = self.get_router( handlers=( self.route_class( h, @@ -89,19 +86,18 @@ async def h( type=ExchangeType.TOPIC, ), ), - ) + ), ) - pub_broker._is_apply_types = True - pub_broker.include_router(r) + pub_broker = self.get_broker(apply_types=True) + pub_broker.include_router(router) await pub_broker.start() - await pub_broker.publish( + await pub_broker.request( "", "in.john.2", queue + "1", - rpc=True, ) assert event.is_set() @@ -109,17 +105,17 @@ async def h( async def test_queue_obj( self, - router: RabbitRouter, - broker: RabbitBroker, queue: str, - event: asyncio.Event, - ): - router.prefix = "test/" + ) -> None: + broker = self.get_broker() + router = self.get_router(prefix="test/") r_queue = RabbitQueue(queue) + event = asyncio.Event() + @router.subscriber(r_queue) - def subscriber(m): + def subscriber(m) -> None: event.set() broker.include_router(router) @@ -130,7 +126,7 @@ def subscriber(m): await asyncio.wait( ( asyncio.create_task( - broker.publish("hello", f"test/{r_queue.name}") + broker.publish("hello", f"test/{r_queue.name}"), ), asyncio.create_task(event.wait()), ), @@ -141,18 +137,18 @@ def subscriber(m): async def test_queue_obj_with_routing_key( self, - router: RabbitRouter, - broker: RabbitBroker, queue: str, - event: asyncio.Event, - ): - router.prefix = "test/" + ) -> None: + event = asyncio.Event() + + broker = self.get_broker() + router = self.get_router(prefix="test/") r_queue = RabbitQueue("useless", routing_key=f"{queue}1") exchange = RabbitExchange(f"{queue}exch") @router.subscriber(r_queue, exchange=exchange) - def subscriber(m): + def subscriber(m) -> None: event.set() broker.include_router(router) @@ -163,7 +159,7 @@ def subscriber(m): await asyncio.wait( ( asyncio.create_task( - broker.publish("hello", f"test/{queue}1", exchange=exchange) + broker.publish("hello", f"test/{queue}1", exchange=exchange), ), asyncio.create_task(event.wait()), ), @@ -174,21 +170,22 @@ def subscriber(m): async def test_delayed_handlers_with_queue( self, - event: asyncio.Event, - router: RabbitRouter, queue: str, - pub_broker: RabbitBroker, - ): - def response(m): + ) -> None: + event = asyncio.Event() + + def response(m) -> None: event.set() r_queue = RabbitQueue(queue) - r = type(router)( - prefix="test/", handlers=(self.route_class(response, queue=r_queue),) + router = self.get_router( + prefix="test/", + handlers=(self.route_class(response, queue=r_queue),), ) - pub_broker.include_router(r) + pub_broker = self.get_broker() + pub_broker.include_router(router) async with pub_broker: await pub_broker.start() @@ -196,7 +193,7 @@ def response(m): await asyncio.wait( ( asyncio.create_task( - pub_broker.publish("hello", f"test/{r_queue.name}") + pub_broker.publish("hello", f"test/{r_queue.name}"), ), asyncio.create_task(event.wait()), ), @@ -206,7 +203,6 @@ def response(m): assert event.is_set() -class TestRouterLocal(RouterLocalTestcase): - broker_class = RabbitRouter +class TestRouterLocal(RabbitMemoryTestcaseConfig, RouterLocalTestcase): route_class = RabbitRoute publisher_class = RabbitPublisher diff --git a/tests/brokers/rabbit/test_rpc.py b/tests/brokers/rabbit/test_rpc.py deleted file mode 100644 index 68e6d12812..0000000000 --- a/tests/brokers/rabbit/test_rpc.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - -from faststream.rabbit import RabbitBroker -from tests.brokers.base.rpc import BrokerRPCTestcase, ReplyAndConsumeForbidden - - -@pytest.mark.rabbit -class TestRPC(BrokerRPCTestcase, ReplyAndConsumeForbidden): - def get_broker(self, apply_types: bool = False) -> RabbitBroker: - return RabbitBroker(apply_types=apply_types) diff --git a/tests/brokers/rabbit/test_schemas.py b/tests/brokers/rabbit/test_schemas.py index da8e4914cb..2224fb976a 100644 --- a/tests/brokers/rabbit/test_schemas.py +++ b/tests/brokers/rabbit/test_schemas.py @@ -1,25 +1,21 @@ from faststream.rabbit import RabbitQueue -def test_same_queue(): +def test_same_queue() -> None: assert ( - len( - { - RabbitQueue("test"): 0, - RabbitQueue("test"): 1, - } - ) + len({ + RabbitQueue("test"): 0, + RabbitQueue("test"): 1, + }) == 1 ) -def test_different_queue_routing_key(): +def test_different_queue_routing_key() -> None: assert ( - len( - { - RabbitQueue("test", routing_key="binding-1"): 0, - RabbitQueue("test", routing_key="binding-2"): 1, - } - ) + len({ + RabbitQueue("test", routing_key="binding-1"): 0, + RabbitQueue("test", routing_key="binding-2"): 1, + }) == 1 ) diff --git a/tests/brokers/rabbit/test_test_client.py b/tests/brokers/rabbit/test_test_client.py index 1d92edf2fa..f03e447829 100644 --- a/tests/brokers/rabbit/test_test_client.py +++ b/tests/brokers/rabbit/test_test_client.py @@ -1,59 +1,39 @@ import asyncio +from typing import Any import pytest from faststream import BaseMiddleware -from faststream.exceptions import SetupError +from faststream.exceptions import SubscriberNotFound from faststream.rabbit import ( ExchangeType, RabbitBroker, RabbitExchange, RabbitQueue, - TestRabbitBroker, ) from faststream.rabbit.annotations import RabbitMessage -from faststream.rabbit.testing import FakeProducer, apply_pattern +from faststream.rabbit.testing import FakeProducer, _is_handler_matches, apply_pattern from tests.brokers.base.testclient import BrokerTestclientTestcase +from .basic import RabbitMemoryTestcaseConfig -@pytest.mark.asyncio -class TestTestclient(BrokerTestclientTestcase): - test_class = TestRabbitBroker - def get_broker(self, apply_types: bool = False) -> RabbitBroker: - return RabbitBroker(apply_types=apply_types) - - def patch_broker(self, broker: RabbitBroker) -> RabbitBroker: - return TestRabbitBroker(broker) - - def get_fake_producer_class(self) -> type: - return FakeProducer - - async def test_rpc_conflicts_reply(self, queue): - broker = self.get_broker() - - async with TestRabbitBroker(broker) as br: - with pytest.raises(SetupError): - await br.publish( - "", - queue, - rpc=True, - reply_to="response", - ) - - @pytest.mark.rabbit +@pytest.mark.asyncio() +class TestTestclient(RabbitMemoryTestcaseConfig, BrokerTestclientTestcase): + @pytest.mark.rabbit() async def test_with_real_testclient( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + broker = self.get_broker() @broker.subscriber(queue) - def subscriber(m): + def subscriber(m) -> None: event.set() - async with TestRabbitBroker(broker, with_real=True) as br: + async with self.patch_broker(broker, with_real=True) as br: await asyncio.wait( ( asyncio.create_task(br.publish("hello", queue)), @@ -64,179 +44,55 @@ def subscriber(m): assert event.is_set() - async def test_respect_routing_key(self): - broker = self.get_broker() - - publisher = broker.publisher( - exchange=RabbitExchange("test", type=ExchangeType.TOPIC), routing_key="up" - ) - - async with TestRabbitBroker(broker): - await publisher.publish("Hi!") - - publisher.mock.assert_called_once_with("Hi!") - - async def test_direct( - self, - queue: str, - ): - broker = self.get_broker() - - @broker.subscriber(queue) - async def handler(m): - return 1 - - @broker.subscriber(queue + "1", exchange="test") - async def handler2(m): - return 2 - - async with TestRabbitBroker(broker) as br: - await br.start() - assert await br.publish("", queue, rpc=True) == 1 - assert await br.publish("", queue + "1", exchange="test", rpc=True) == 2 - assert None is await br.publish("", exchange="test2", rpc=True) - - async def test_fanout( + async def test_direct_not_found( self, queue: str, - mock, - ): + ) -> None: broker = self.get_broker() - exch = RabbitExchange("test", type=ExchangeType.FANOUT) - - @broker.subscriber(queue, exchange=exch) - async def handler(m): - mock() - - async with TestRabbitBroker(broker) as br: - await br.publish("", exchange=exch, rpc=True) - - assert None is await br.publish("", exchange="test2", rpc=True) - - assert mock.call_count == 1 - - async def test_any_topic_routing(self): - broker = self.get_broker() - - exch = RabbitExchange("test", type=ExchangeType.TOPIC) - - @broker.subscriber( - RabbitQueue("test", routing_key="test.*.subj.*"), - exchange=exch, - ) - def subscriber(msg): ... - - async with TestRabbitBroker(broker) as br: - await br.publish("hello", "test.a.subj.b", exchange=exch) - subscriber.mock.assert_called_once_with("hello") - - async def test_ending_topic_routing(self): - broker = self.get_broker() - - exch = RabbitExchange("test", type=ExchangeType.TOPIC) - - @broker.subscriber( - RabbitQueue("test", routing_key="test.#"), - exchange=exch, - ) - def subscriber(msg): ... - - async with TestRabbitBroker(broker) as br: - await br.publish("hello", "test.a.subj.b", exchange=exch) - subscriber.mock.assert_called_once_with("hello") - - async def test_mixed_topic_routing(self): - broker = self.get_broker() - - exch = RabbitExchange("test", type=ExchangeType.TOPIC) - - @broker.subscriber( - RabbitQueue("test", routing_key="*.*.subj.#"), - exchange=exch, - ) - def subscriber(msg): ... - - async with TestRabbitBroker(broker) as br: - await br.publish("hello", "test.a.subj.b.c", exchange=exch) - subscriber.mock.assert_called_once_with("hello") - - async def test_header(self): - broker = self.get_broker() - - q1 = RabbitQueue( - "test-queue-2", - bind_arguments={"key": 2, "key2": 2, "x-match": "any"}, - ) - q2 = RabbitQueue( - "test-queue-3", - bind_arguments={"key": 2, "key2": 2, "x-match": "all"}, - ) - q3 = RabbitQueue( - "test-queue-4", - bind_arguments={}, - ) - exch = RabbitExchange("exchange", type=ExchangeType.HEADERS) - - @broker.subscriber(q2, exch) - async def handler2(msg): - return 2 - - @broker.subscriber(q1, exch) - async def handler(msg): - return 1 - - @broker.subscriber(q3, exch) - async def handler3(msg): - return 3 - - async with TestRabbitBroker(broker) as br: - assert ( - await br.publish(exchange=exch, rpc=True, headers={"key": 2, "key2": 2}) - == 2 - ) - assert await br.publish(exchange=exch, rpc=True, headers={"key": 2}) == 1 - assert await br.publish(exchange=exch, rpc=True, headers={}) == 3 + async with self.patch_broker(broker) as br: + with pytest.raises(SubscriberNotFound): + await br.request("", "") async def test_consume_manual_ack( self, queue: str, exchange: RabbitExchange, - ): + ) -> None: broker = self.get_broker(apply_types=True) consume = asyncio.Event() consume2 = asyncio.Event() consume3 = asyncio.Event() - @broker.subscriber(queue=queue, exchange=exchange, retry=1) - async def handler(msg: RabbitMessage): + @broker.subscriber(queue=queue, exchange=exchange) + async def handler(msg: RabbitMessage) -> None: await msg.raw_message.ack() consume.set() - @broker.subscriber(queue=queue + "1", exchange=exchange, retry=1) - async def handler2(msg: RabbitMessage): + @broker.subscriber(queue=queue + "1", exchange=exchange) + async def handler2(msg: RabbitMessage) -> None: await msg.raw_message.nack() consume2.set() - raise ValueError() + raise ValueError - @broker.subscriber(queue=queue + "2", exchange=exchange, retry=1) - async def handler3(msg: RabbitMessage): + @broker.subscriber(queue=queue + "2", exchange=exchange) + async def handler3(msg: RabbitMessage) -> None: await msg.raw_message.reject() consume3.set() - raise ValueError() + raise ValueError - async with TestRabbitBroker(broker) as br: + async with self.patch_broker(broker) as br: await asyncio.wait( ( asyncio.create_task( - br.publish("hello", queue=queue, exchange=exchange) + br.publish("hello", queue=queue, exchange=exchange), ), asyncio.create_task( - br.publish("hello", queue=queue + "1", exchange=exchange) + br.publish("hello", queue=queue + "1", exchange=exchange), ), asyncio.create_task( - br.publish("hello", queue=queue + "2", exchange=exchange) + br.publish("hello", queue=queue + "2", exchange=exchange), ), asyncio.create_task(consume.wait()), asyncio.create_task(consume2.wait()), @@ -249,7 +105,7 @@ async def handler3(msg: RabbitMessage): assert consume2.is_set() assert consume3.is_set() - async def test_respect_middleware(self, queue): + async def test_respect_middleware(self, queue: str) -> None: routes = [] class Middleware(BaseMiddleware): @@ -257,22 +113,22 @@ async def on_receive(self) -> None: routes.append(None) return await super().on_receive() - broker = RabbitBroker(middlewares=(Middleware,)) + broker = self.get_broker(middlewares=(Middleware,)) @broker.subscriber(queue) - async def h1(msg): ... + async def h1(msg) -> None: ... @broker.subscriber(queue + "1") - async def h2(msg): ... + async def h2(msg) -> None: ... - async with TestRabbitBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("", queue) await br.publish("", queue + "1") assert len(routes) == 2 - @pytest.mark.rabbit - async def test_real_respect_middleware(self, queue): + @pytest.mark.rabbit() + async def test_real_respect_middleware(self, queue: str) -> None: routes = [] class Middleware(BaseMiddleware): @@ -280,15 +136,15 @@ async def on_receive(self) -> None: routes.append(None) return await super().on_receive() - broker = RabbitBroker(middlewares=(Middleware,)) + broker = self.get_broker(middlewares=(Middleware,)) @broker.subscriber(queue) - async def h1(msg): ... + async def h1(msg) -> None: ... @broker.subscriber(queue + "1") - async def h2(msg): ... + async def h2(msg) -> None: ... - async with TestRabbitBroker(broker, with_real=True) as br: + async with self.patch_broker(broker, with_real=True) as br: await br.publish("", queue) await br.publish("", queue + "1") await h1.wait_call(3) @@ -296,24 +152,25 @@ async def h2(msg): ... assert len(routes) == 2 - @pytest.mark.rabbit - async def test_broker_gets_patched_attrs_within_cm(self): - await super().test_broker_gets_patched_attrs_within_cm() + @pytest.mark.rabbit() + async def test_broker_gets_patched_attrs_within_cm(self) -> None: + await super().test_broker_gets_patched_attrs_within_cm(FakeProducer) - @pytest.mark.rabbit - async def test_broker_with_real_doesnt_get_patched(self): + @pytest.mark.rabbit() + async def test_broker_with_real_doesnt_get_patched(self) -> None: await super().test_broker_with_real_doesnt_get_patched() - @pytest.mark.rabbit + @pytest.mark.rabbit() async def test_broker_with_real_patches_publishers_and_subscribers( - self, queue: str - ): + self, + queue: str, + ) -> None: await super().test_broker_with_real_patches_publishers_and_subscribers(queue) @pytest.mark.parametrize( ("pattern", "current", "result"), - [ + ( pytest.param("#", "1.2.3", True, id="#"), pytest.param("*", "1", True, id="*"), pytest.param("*", "1.2", False, id="* - broken"), @@ -324,14 +181,156 @@ async def test_broker_with_real_patches_publishers_and_subscribers( pytest.param("#.test.*.*", "1.2.test.1.2", True, id="#.test.*."), pytest.param("#.test.*.*.*", "1.2.test.1.2", False, id="#.test.*.*.* - broken"), pytest.param( - "#.test.*.test.#", "1.2.test.1.test.1.2", True, id="#.test.*.test.#" + "#.test.*.test.#", + "1.2.test.1.test.1.2", + True, + id="#.test.*.test.#", ), pytest.param("#.*.test", "1.2.2.test", True, id="#.*.test"), pytest.param("#.2.*.test", "1.2.2.test", True, id="#.2.*.test"), pytest.param("#.*.*.test", "1.2.2.test", True, id="#.*.*.test"), pytest.param("*.*.*.test", "1.2.test", False, id="*.*.*.test - broken"), pytest.param("#.*.*.test", "1.2.test", False, id="#.*.*.test - broken"), - ], + ), ) -def test(pattern: str, current: str, result: bool): +def test(pattern: str, current: str, result: bool) -> None: assert apply_pattern(pattern, current) == result + + +exch_direct = RabbitExchange("exchange", auto_delete=True, type=ExchangeType.DIRECT) +exch_fanout = RabbitExchange("exchange", auto_delete=True, type=ExchangeType.FANOUT) +exch_topic = RabbitExchange("exchange", auto_delete=True, type=ExchangeType.TOPIC) +exch_headers = RabbitExchange("exchange", auto_delete=True, type=ExchangeType.HEADERS) +reqular_queue = RabbitQueue("test-reqular-queue", auto_delete=True) + +routing_key_queue = RabbitQueue( + "test-routing-key-queue", auto_delete=True, routing_key="*.info" +) +one_key_queue = RabbitQueue( + "test-one-key-queue", auto_delete=True, bind_arguments={"key": 1} +) +any_keys_queue = RabbitQueue( + "test-any-keys-queue", + auto_delete=True, + bind_arguments={"key": 2, "key2": 2, "x-match": "any"}, +) +all_keys_queue = RabbitQueue( + "test-all-keys-queue", + auto_delete=True, + bind_arguments={"key": 2, "key2": 2, "x-match": "all"}, +) + +broker = RabbitBroker() + + +@pytest.mark.parametrize( + ( + "queue", + "exchange", + "routing_key", + "headers", + "expected_result", + ), + ( + pytest.param( + reqular_queue, + exch_direct, + reqular_queue.routing(), + {}, + True, + id="direct match", + ), + pytest.param( + reqular_queue, + exch_direct, + "wrong key", + {}, + False, + id="direct mismatch", + ), + pytest.param( + reqular_queue, + exch_fanout, + "", + {}, + True, + id="fanout match", + ), + pytest.param( + routing_key_queue, + exch_topic, + "log.info", + {}, + True, + id="topic match", + ), + pytest.param( + routing_key_queue, + exch_topic, + "log.wrong", + {}, + False, + id="topic mismatch", + ), + pytest.param( + one_key_queue, + exch_headers, + "", + {"key": 1}, + True, + id="one header match", + ), + pytest.param( + one_key_queue, + exch_headers, + "", + {"key": "wrong"}, + False, + id="one header mismatch", + ), + pytest.param( + any_keys_queue, + exch_headers, + "", + {"key2": 2}, + True, + id="any headers match", + ), + pytest.param( + any_keys_queue, + exch_headers, + "", + {"key2": "wrong"}, + False, + id="any headers mismatch", + ), + pytest.param( + all_keys_queue, + exch_headers, + "", + {"key": 2, "key2": 2}, + True, + id="all headers match", + ), + pytest.param( + all_keys_queue, + exch_headers, + "", + {"key": "wrong", "key2": 2}, + False, + id="all headers mismatch", + ), + ), +) +def test_in_memory_routing( + queue: str, + exchange: RabbitExchange, + routing_key: str, + headers: dict[str, Any], + expected_result: bool, +) -> None: + subscriber = broker.subscriber(queue, exchange) + assert ( + _is_handler_matches(subscriber, routing_key, headers, exchange) + is expected_result + ) diff --git a/tests/brokers/rabbit/test_test_reentrancy.py b/tests/brokers/rabbit/test_test_reentrancy.py index 4ee4819959..5b4c804255 100644 --- a/tests/brokers/rabbit/test_test_reentrancy.py +++ b/tests/brokers/rabbit/test_test_reentrancy.py @@ -15,11 +15,11 @@ async def on_input_data(msg: int): @broker.subscriber("output_data") -async def on_output_data(msg: int): +async def on_output_data(msg: int) -> None: pass -async def _test_with_broker(with_real: bool): +async def _test_with_broker(with_real: bool) -> None: async with TestRabbitBroker(broker, with_real=with_real) as tester: await tester.publish(1, "input_data") @@ -30,22 +30,22 @@ async def _test_with_broker(with_real: bool): on_output_data.mock.assert_called_once_with(2) -@pytest.mark.asyncio -async def test_with_fake_broker(): +@pytest.mark.asyncio() +async def test_with_fake_broker() -> None: await _test_with_broker(False) await _test_with_broker(False) -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_with_real_broker(): +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_with_real_broker() -> None: await _test_with_broker(True) await _test_with_broker(True) -async def _test_with_temp_subscriber(): +async def _test_with_temp_subscriber() -> None: @broker.subscriber("output_data") - async def on_output_data(msg: int): + async def on_output_data(msg: int) -> None: pass async with TestRabbitBroker(broker) as tester: @@ -58,13 +58,13 @@ async def on_output_data(msg: int): on_output_data.mock.assert_called_once_with(2) -@pytest.mark.asyncio +@pytest.mark.asyncio() @pytest.mark.skip( reason=( "Failed due `on_output_data` subscriber creates inside test and doesn't removed after " "https://github.com/ag2ai/faststream/issues/556" ) ) -async def test_with_temp_subscriber(): +async def test_with_temp_subscriber() -> None: await _test_with_temp_subscriber() await _test_with_temp_subscriber() diff --git a/tests/brokers/rabbit/test_url_builder.py b/tests/brokers/rabbit/test_url_builder.py index 0185207145..113708f7a0 100644 --- a/tests/brokers/rabbit/test_url_builder.py +++ b/tests/brokers/rabbit/test_url_builder.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any import pytest from yarl import URL @@ -8,7 +8,7 @@ @pytest.mark.parametrize( ("url_kwargs", "expected_url"), - [ + ( pytest.param( {}, URL("amqp://guest:guest@localhost:5672/"), # pragma: allowlist secret @@ -31,8 +31,8 @@ ), id="exotic virtualhost", ), - ], + ), ) -def test_unpack_args(url_kwargs: Dict[str, Any], expected_url: URL) -> None: +def test_unpack_args(url_kwargs: dict[str, Any], expected_url: URL) -> None: url = build_url(**url_kwargs) assert url == expected_url diff --git a/tests/brokers/redis/basic.py b/tests/brokers/redis/basic.py new file mode 100644 index 0000000000..11f424017c --- /dev/null +++ b/tests/brokers/redis/basic.py @@ -0,0 +1,24 @@ +from typing import Any + +from faststream.redis import RedisBroker, RedisRouter, TestRedisBroker +from tests.brokers.base.basic import BaseTestcaseConfig + + +class RedisTestcaseConfig(BaseTestcaseConfig): + def get_broker( + self, + apply_types: bool = False, + **kwargs: Any, + ) -> RedisBroker: + return RedisBroker(apply_types=apply_types, **kwargs) + + def patch_broker(self, broker: RedisBroker, **kwargs: Any) -> RedisBroker: + return broker + + def get_router(self, **kwargs: Any) -> RedisRouter: + return RedisRouter(**kwargs) + + +class RedisMemoryTestcaseConfig(RedisTestcaseConfig): + def patch_broker(self, broker: RedisBroker, **kwargs: Any) -> RedisBroker: + return TestRedisBroker(broker, **kwargs) diff --git a/tests/brokers/redis/conftest.py b/tests/brokers/redis/conftest.py index 2066975eb8..abaa236955 100644 --- a/tests/brokers/redis/conftest.py +++ b/tests/brokers/redis/conftest.py @@ -1,20 +1,15 @@ from dataclasses import dataclass import pytest -import pytest_asyncio -from faststream.redis import ( - RedisBroker, - RedisRouter, - TestRedisBroker, -) +from faststream.redis import RedisRouter @dataclass class Settings: - url = "redis://localhost:6379" # pragma: allowlist secret - host = "localhost" - port = 6379 + url: str = "redis://localhost:6379" # pragma: allowlist secret + host: str = "localhost" + port: int = 6379 @pytest.fixture(scope="session") @@ -22,27 +17,6 @@ def settings(): return Settings() -@pytest.fixture +@pytest.fixture() def router(): return RedisRouter() - - -@pytest_asyncio.fixture() -async def broker(settings): - broker = RedisBroker(settings.url, apply_types=False) - async with broker: - yield broker - - -@pytest_asyncio.fixture() -async def full_broker(settings): - broker = RedisBroker(settings.url) - async with broker: - yield broker - - -@pytest_asyncio.fixture() -async def test_broker(): - broker = RedisBroker() - async with TestRedisBroker(broker) as br: - yield br diff --git a/tests/brokers/redis/future/test_fastapi.py b/tests/brokers/redis/future/test_fastapi.py index f528ac9ab4..06fc746de9 100644 --- a/tests/brokers/redis/future/test_fastapi.py +++ b/tests/brokers/redis/future/test_fastapi.py @@ -1,11 +1,11 @@ import pytest +from faststream.redis.broker.router import RedisRouter from faststream.redis.fastapi import RedisRouter as StreamRouter -from faststream.redis.router import RedisRouter from tests.brokers.base.future.fastapi import FastapiTestCase -@pytest.mark.redis +@pytest.mark.redis() class TestRouter(FastapiTestCase): router_class = StreamRouter broker_router_class = RedisRouter diff --git a/tests/brokers/redis/test_config.py b/tests/brokers/redis/test_config.py new file mode 100644 index 0000000000..bbcaca0fa1 --- /dev/null +++ b/tests/brokers/redis/test_config.py @@ -0,0 +1,69 @@ +from unittest.mock import MagicMock + +from faststream import AckPolicy +from faststream.redis import ListSub, PubSub, StreamSub +from faststream.redis.subscriber.config import RedisSubscriberConfig + + +def test_channel_sub() -> None: + config = RedisSubscriberConfig( + _outer_config=MagicMock(), channel_sub=PubSub("test_channel") + ) + assert config.ack_policy is AckPolicy.DO_NOTHING + + +def test_list_sub() -> None: + config = RedisSubscriberConfig( + _outer_config=MagicMock(), list_sub=ListSub("test_list") + ) + assert config.ack_policy is AckPolicy.DO_NOTHING + + +def test_stream_sub() -> None: + config = RedisSubscriberConfig( + _outer_config=MagicMock(), stream_sub=StreamSub("test_stream") + ) + assert config.ack_policy is AckPolicy.DO_NOTHING + + +def test_stream_with_group() -> None: + config = RedisSubscriberConfig( + _outer_config=MagicMock(), + stream_sub=StreamSub( + "test_stream", + group="test_group", + consumer="test_consumer", + ), + ) + assert config.ack_policy is AckPolicy.REJECT_ON_ERROR + + +def test_custom_ack() -> None: + config = RedisSubscriberConfig( + _outer_config=MagicMock(), + stream_sub=StreamSub( + "test_stream", + group="test_group", + consumer="test_consumer", + ), + _ack_policy=AckPolicy.ACK, + ) + assert config.ack_policy is AckPolicy.ACK + + +def test_stream_sub_with_no_ack_group() -> None: + config = RedisSubscriberConfig( + _outer_config=MagicMock(), + stream_sub=StreamSub( + "test_stream", + group="test_group", + consumer="test_consumer", + no_ack=True, + ), + ) + assert config.ack_policy is AckPolicy.DO_NOTHING + + +def test_no_ack() -> None: + config = RedisSubscriberConfig(_outer_config=MagicMock(), _no_ack=True) + assert config.ack_policy is AckPolicy.DO_NOTHING diff --git a/tests/brokers/redis/test_connect.py b/tests/brokers/redis/test_connect.py index 297487d629..fcbf963b1f 100644 --- a/tests/brokers/redis/test_connect.py +++ b/tests/brokers/redis/test_connect.py @@ -1,42 +1,29 @@ +from typing import Any + import pytest from faststream.redis import RedisBroker from tests.brokers.base.connection import BrokerConnectionTestcase +from .conftest import Settings + -@pytest.mark.redis +@pytest.mark.redis() class TestConnection(BrokerConnectionTestcase): broker = RedisBroker - def get_broker_args(self, settings): + def get_broker_args(self, settings: Settings) -> dict[str, Any]: return { "url": settings.url, "host": settings.host, "port": settings.port, } - @pytest.mark.asyncio - async def test_init_connect_by_raw_data(self, settings): + @pytest.mark.asyncio() + async def test_init_connect_by_raw_data(self, settings: Settings) -> None: async with RedisBroker( - "redis://localhost:6378", # will be ignored + "redis://localhost:63781", # will be overridden by port and host options host=settings.host, port=settings.port, ) as broker: assert await self.ping(broker) - - @pytest.mark.asyncio - async def test_connect_merge_kwargs_with_priority(self, settings): - broker = self.broker(host="fake-host", port=6377) # kwargs will be ignored - await broker.connect( - host=settings.host, - port=settings.port, - ) - assert await self.ping(broker) - await broker.close() - - @pytest.mark.asyncio - async def test_connect_merge_args_and_kwargs_native(self, settings): - broker = self.broker("fake-url") # will be ignored - await broker.connect(url=settings.url) - assert await self.ping(broker) - await broker.close() diff --git a/tests/brokers/redis/test_consume.py b/tests/brokers/redis/test_consume.py index 0fe86a8c84..76397e3ae0 100644 --- a/tests/brokers/redis/test_consume.py +++ b/tests/brokers/redis/test_consume.py @@ -1,57 +1,62 @@ import asyncio -from typing import List -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, call, patch import pytest from redis.asyncio import Redis -from faststream.redis import ListSub, PubSub, RedisBroker, RedisMessage, StreamSub -from faststream.redis.annotations import RedisStreamMessage +from faststream import AckPolicy +from faststream.redis import ( + ListSub, + PubSub, + RedisMessage, + RedisStreamMessage, + StreamSub, +) from tests.brokers.base.consume import BrokerRealConsumeTestcase from tests.tools import spy_decorator +from .basic import RedisTestcaseConfig -@pytest.mark.redis -@pytest.mark.asyncio -class TestConsume(BrokerRealConsumeTestcase): - def get_broker(self, apply_types: bool = False): - return RedisBroker(apply_types=apply_types) +@pytest.mark.redis() +@pytest.mark.asyncio() +class TestConsume(RedisTestcaseConfig, BrokerRealConsumeTestcase): async def test_consume_native( self, - event: asyncio.Event, mock: MagicMock, queue: str, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber(queue) - async def handler(msg): + async def handler(msg) -> None: mock(msg) event.set() async with self.patch_broker(consume_broker) as br: await br.start() + result = await br._connection.publish(queue, "hello") await asyncio.wait( - ( - asyncio.create_task(br._connection.publish(queue, "hello")), - asyncio.create_task(event.wait()), - ), + (asyncio.create_task(event.wait()),), timeout=3, ) + assert result == 1, result mock.assert_called_once_with(b"hello") async def test_pattern_with_path( self, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber("test.{name}") - async def handler(msg): + async def handler(msg) -> None: mock(msg) event.set() @@ -70,13 +75,14 @@ async def handler(msg): async def test_pattern_without_path( self, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber(PubSub("test.*", pattern=True)) - async def handler(msg): + async def handler(msg) -> None: mock(msg) event.set() @@ -93,26 +99,58 @@ async def handler(msg): mock.assert_called_once_with("hello") + async def test_concurrent_consume_channel( + self, + queue: str, + mock: MagicMock, + ) -> None: + event = asyncio.Event() + event2 = asyncio.Event() -@pytest.mark.redis -@pytest.mark.asyncio -class TestConsumeList: - def get_broker(self, apply_types: bool = False): - return RedisBroker(apply_types=apply_types) + consume_broker = self.get_broker() - def patch_broker(self, broker): - return broker + @consume_broker.subscriber(channel=PubSub(queue), max_workers=2) + async def handler(msg): + mock() + if event.is_set(): + event2.set() + else: + event.set() + await asyncio.sleep(0.1) + async with self.patch_broker(consume_broker) as br: + await br.start() + + for i in range(5): + await br.publish(i, queue) + + await asyncio.wait( + ( + asyncio.create_task(event.wait()), + asyncio.create_task(event2.wait()), + ), + timeout=3, + ) + + assert event.is_set() + assert event2.is_set() + assert mock.call_count == 2, mock.call_count + + +@pytest.mark.redis() +@pytest.mark.asyncio() +class TestConsumeList(RedisTestcaseConfig): async def test_consume_list( self, - event: asyncio.Event, queue: str, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber(list=queue) - async def handler(msg): + async def handler(msg) -> None: mock(msg) event.set() @@ -131,14 +169,15 @@ async def handler(msg): async def test_consume_list_native( self, - event: asyncio.Event, queue: str, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber(list=queue) - async def handler(msg): + async def handler(msg) -> None: mock(msg) event.set() @@ -155,19 +194,20 @@ async def handler(msg): mock.assert_called_once_with(b"hello") - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_list_batch_with_one( self, queue: str, - event: asyncio.Event, - mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber( - list=ListSub(queue, batch=True, polling_interval=0.01) + list=ListSub(queue, batch=True, polling_interval=0.01), ) - async def handler(msg): + async def handler(msg) -> None: mock(msg) event.set() @@ -184,26 +224,27 @@ async def handler(msg): assert event.is_set() mock.assert_called_once_with(["hi"]) - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_list_batch_headers( self, queue: str, - event: asyncio.Event, - mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber( - list=ListSub(queue, batch=True, polling_interval=0.01) + list=ListSub(queue, batch=True, polling_interval=0.01), ) - def subscriber(m, msg: RedisMessage): + def subscriber(m, msg: RedisMessage) -> None: check = all( ( msg.headers, msg.headers["correlation_id"] == msg.batch_headers[0]["correlation_id"], msg.headers.get("custom") == "1", - ) + ), ) mock(check) event.set() @@ -213,7 +254,7 @@ def subscriber(m, msg: RedisMessage): await asyncio.wait( ( asyncio.create_task( - br.publish("", list=queue, headers={"custom": "1"}) + br.publish("", list=queue, headers={"custom": "1"}), ), asyncio.create_task(event.wait()), ), @@ -223,19 +264,19 @@ def subscriber(m, msg: RedisMessage): assert event.is_set() mock.assert_called_once_with(True) - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_list_batch( self, queue: str, - ): + ) -> None: consume_broker = self.get_broker(apply_types=True) msgs_queue = asyncio.Queue(maxsize=1) @consume_broker.subscriber( - list=ListSub(queue, batch=True, polling_interval=0.01) + list=ListSub(queue, batch=True, polling_interval=0.01), ) - async def handler(msg): + async def handler(msg) -> None: await msgs_queue.put(msg) async with self.patch_broker(consume_broker) as br: @@ -250,11 +291,11 @@ async def handler(msg): assert [{1, "hi"}] == [set(r.result()) for r in result] - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_list_batch_complex( self, queue: str, - ): + ) -> None: consume_broker = self.get_broker(apply_types=True) from pydantic import BaseModel @@ -268,9 +309,9 @@ def __hash__(self): msgs_queue = asyncio.Queue(maxsize=1) @consume_broker.subscriber( - list=ListSub(queue, batch=True, polling_interval=0.01) + list=ListSub(queue, batch=True, polling_interval=0.01), ) - async def handler(msg: List[Data]): + async def handler(msg: list[Data]) -> None: await msgs_queue.put(msg) async with self.patch_broker(consume_broker) as br: @@ -285,19 +326,19 @@ async def handler(msg: List[Data]): assert [{Data(m="hi"), Data(m="again")}] == [set(r.result()) for r in result] - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_list_batch_native( self, queue: str, - ): + ) -> None: consume_broker = self.get_broker() msgs_queue = asyncio.Queue(maxsize=1) @consume_broker.subscriber( - list=ListSub(queue, batch=True, polling_interval=0.01) + list=ListSub(queue, batch=True, polling_interval=0.01), ) - async def handler(msg): + async def handler(msg) -> None: await msgs_queue.put(msg) async with self.patch_broker(consume_broker) as br: @@ -315,8 +356,7 @@ async def handler(msg): async def test_get_one( self, queue: str, - event: asyncio.Event, - ): + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber(list=queue) @@ -325,11 +365,11 @@ async def test_get_one( message = None - async def consume(): + async def consume() -> None: nonlocal message message = await subscriber.get_one(timeout=5) - async def publish(): + async def publish() -> None: await br.publish("test_message", list=queue) await asyncio.wait( @@ -347,7 +387,7 @@ async def test_get_one_timeout( self, queue: str, mock: MagicMock, - ): + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber(list=queue) @@ -357,27 +397,87 @@ async def test_get_one_timeout( mock(await subscriber.get_one(timeout=1e-24)) mock.assert_called_once_with(None) + async def test_concurrent_consume_list( + self, + queue: str, + mock: MagicMock, + ) -> None: + event = asyncio.Event() + event2 = asyncio.Event() + + consume_broker = self.get_broker() + + @consume_broker.subscriber(list=ListSub(queue), max_workers=2) + async def handler(msg): + mock() + if event.is_set(): + event2.set() + else: + event.set() + await asyncio.sleep(0.1) + + async with self.patch_broker(consume_broker) as br: + await br.start() + + for i in range(5): + await br.publish(i, list=queue) + + await asyncio.wait( + ( + asyncio.create_task(event.wait()), + asyncio.create_task(event2.wait()), + ), + timeout=3, + ) + + assert event.is_set() + assert event2.is_set() + assert mock.call_count == 2, mock.call_count + + async def test_iterator( + self, + queue: str, + ) -> None: + expected_messages = ("test_message_1", "test_message_2") + + broker = self.get_broker(apply_types=True) + subscriber = broker.subscriber(list=queue) + + async with self.patch_broker(broker) as br: + await br.start() + + async def publish_test_message(): + for msg in expected_messages: + await br.publish(msg, list=queue) -@pytest.mark.redis -@pytest.mark.asyncio -class TestConsumeStream: - def get_broker(self, apply_types: bool = False): - return RedisBroker(apply_types=apply_types) + _ = await asyncio.create_task(publish_test_message()) - def patch_broker(self, broker): - return broker + index_message = 0 + async for msg in subscriber: + result_message = await msg.decode() - @pytest.mark.slow + assert result_message == expected_messages[index_message] + + index_message += 1 + if index_message >= len(expected_messages): + break + + +@pytest.mark.redis() +@pytest.mark.asyncio() +class TestConsumeStream(RedisTestcaseConfig): + @pytest.mark.slow() async def test_consume_stream( self, - event: asyncio.Event, mock: MagicMock, - queue, - ): + queue: str, + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber(stream=StreamSub(queue, polling_interval=10)) - async def handler(msg): + async def handler(msg) -> None: mock(msg) event.set() @@ -394,13 +494,13 @@ async def handler(msg): mock.assert_called_once_with("hello") - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_stream_with_big_interval( self, event: asyncio.Event, mock: MagicMock, - queue, - ): + queue: str, + ) -> None: consume_broker = self.get_broker() @consume_broker.subscriber(stream=StreamSub(queue, polling_interval=100000)) @@ -420,17 +520,18 @@ async def handler(msg): mock.assert_called_once_with("hello") - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_stream_native( self, - event: asyncio.Event, mock: MagicMock, - queue, - ): + queue: str, + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber(stream=StreamSub(queue, polling_interval=10)) - async def handler(msg): + async def handler(msg) -> None: mock(msg) event.set() @@ -440,7 +541,7 @@ async def handler(msg): await asyncio.wait( ( asyncio.create_task( - br._connection.xadd(queue, {"message": "hello"}) + br._connection.xadd(queue, {"message": "hello"}), ), asyncio.create_task(event.wait()), ), @@ -449,19 +550,20 @@ async def handler(msg): mock.assert_called_once_with({"message": "hello"}) - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_stream_batch( self, - event: asyncio.Event, mock: MagicMock, - queue, - ): + queue: str, + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber( - stream=StreamSub(queue, polling_interval=10, batch=True) + stream=StreamSub(queue, polling_interval=10, batch=True), ) - async def handler(msg): + async def handler(msg) -> None: mock(msg) event.set() @@ -478,26 +580,27 @@ async def handler(msg): mock.assert_called_once_with(["hello"]) - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_stream_batch_headers( self, queue: str, - event: asyncio.Event, - mock, - ): + mock: MagicMock, + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber( - stream=StreamSub(queue, polling_interval=10, batch=True) + stream=StreamSub(queue, polling_interval=10, batch=True), ) - def subscriber(m, msg: RedisMessage): + def subscriber(m, msg: RedisMessage) -> None: check = all( ( msg.headers, msg.headers["correlation_id"] == msg.batch_headers[0]["correlation_id"], msg.headers.get("custom") == "1", - ) + ), ) mock(check) event.set() @@ -507,7 +610,7 @@ def subscriber(m, msg: RedisMessage): await asyncio.wait( ( asyncio.create_task( - br.publish("", stream=queue, headers={"custom": "1"}) + br.publish("", stream=queue, headers={"custom": "1"}), ), asyncio.create_task(event.wait()), ), @@ -517,11 +620,11 @@ def subscriber(m, msg: RedisMessage): assert event.is_set() mock.assert_called_once_with(True) - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_stream_batch_complex( self, - queue, - ): + queue: str, + ) -> None: consume_broker = self.get_broker(apply_types=True) from pydantic import BaseModel @@ -532,9 +635,9 @@ class Data(BaseModel): msgs_queue = asyncio.Queue(maxsize=1) @consume_broker.subscriber( - stream=StreamSub(queue, polling_interval=10, batch=True) + stream=StreamSub(queue, polling_interval=10, batch=True), ) - async def handler(msg: List[Data]): + async def handler(msg: list[Data]) -> None: await msgs_queue.put(msg) async with self.patch_broker(consume_broker) as br: @@ -549,19 +652,20 @@ async def handler(msg: List[Data]): assert next(iter(result)).result() == [Data(m="hi")] - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_stream_batch_native( self, - event: asyncio.Event, mock: MagicMock, - queue, - ): + queue: str, + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker() @consume_broker.subscriber( - stream=StreamSub(queue, polling_interval=10, batch=True) + stream=StreamSub(queue, polling_interval=10, batch=True), ) - async def handler(msg): + async def handler(msg) -> None: mock(msg) event.set() @@ -571,7 +675,7 @@ async def handler(msg): await asyncio.wait( ( asyncio.create_task( - br._connection.xadd(queue, {"message": "hello"}) + br._connection.xadd(queue, {"message": "hello"}), ), asyncio.create_task(event.wait()), ), @@ -583,40 +687,41 @@ async def handler(msg): async def test_consume_group( self, queue: str, - ): + ) -> None: consume_broker = self.get_broker() @consume_broker.subscriber( - stream=StreamSub(queue, group="group", consumer=queue) + stream=StreamSub(queue, group="group", consumer=queue), ) - async def handler(msg: RedisMessage): ... + async def handler(msg: RedisMessage) -> None: ... - assert next(iter(consume_broker._subscribers.values())).last_id == "$" + assert next(iter(consume_broker._subscribers)).last_id == "$" async def test_consume_group_with_last_id( self, queue: str, - ): + ) -> None: consume_broker = self.get_broker() @consume_broker.subscriber( - stream=StreamSub(queue, group="group", consumer=queue, last_id="0") + stream=StreamSub(queue, group="group", consumer=queue, last_id="0"), ) - async def handler(msg: RedisMessage): ... + async def handler(msg: RedisMessage) -> None: ... - assert next(iter(consume_broker._subscribers.values())).last_id == "0" + assert next(iter(consume_broker._subscribers)).last_id == "0" async def test_consume_nack( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber( - stream=StreamSub(queue, group="group", consumer=queue) + stream=StreamSub(queue, group="group", consumer=queue), ) - async def handler(msg: RedisMessage): + async def handler(msg: RedisMessage) -> None: event.set() await msg.nack() @@ -639,14 +744,15 @@ async def handler(msg: RedisMessage): async def test_consume_ack( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + consume_broker = self.get_broker(apply_types=True) @consume_broker.subscriber( - stream=StreamSub(queue, group="group", consumer=queue) + stream=StreamSub(queue, group="group", consumer=queue), ) - async def handler(msg: RedisMessage): + async def handler(msg: RedisMessage) -> None: event.set() async with self.patch_broker(consume_broker) as br: @@ -673,7 +779,7 @@ async def test_consume_and_delete_acked( @consume_broker.subscriber( stream=StreamSub(queue, group="group", consumer=queue) ) - async def handler(msg: RedisStreamMessage): + async def handler(msg: RedisStreamMessage) -> None: event.set() await msg.delete(consume_broker._connection) @@ -703,12 +809,12 @@ async def test_consume_and_delete_nacked( @consume_broker.subscriber( stream=StreamSub(queue, group="group", consumer=queue), - no_ack=True, + ack_policy=AckPolicy.DO_NOTHING, ) - async def handler(msg: RedisStreamMessage): - event.set() + async def handler(msg: RedisStreamMessage) -> None: assert not msg.committed await msg.delete(consume_broker._connection) + event.set() async with self.patch_broker(consume_broker) as br: await br.start() @@ -732,7 +838,7 @@ async def handler(msg: RedisStreamMessage): async def test_get_one( self, queue: str, - ): + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber(stream=queue) @@ -741,11 +847,11 @@ async def test_get_one( message = None - async def consume(): + async def consume() -> None: nonlocal message message = await subscriber.get_one(timeout=3) - async def publish(): + async def publish() -> None: await asyncio.sleep(0.1) await br.publish("test_message", stream=queue) @@ -764,7 +870,7 @@ async def test_get_one_timeout( self, queue: str, mock: MagicMock, - ): + ) -> None: broker = self.get_broker(apply_types=True) subscriber = broker.subscriber(stream=queue) @@ -773,3 +879,78 @@ async def test_get_one_timeout( mock(await subscriber.get_one(timeout=1e-24)) mock.assert_called_once_with(None) + + async def test_concurrent_consume_stream( + self, + queue: str, + mock: MagicMock, + ) -> None: + event = asyncio.Event() + event2 = asyncio.Event() + + consume_broker = self.get_broker() + + @consume_broker.subscriber(stream=StreamSub(queue), max_workers=2) + async def handler(msg: RedisStreamMessage) -> None: + mock() + if event.is_set(): + event2.set() + else: + event.set() + await asyncio.sleep(0.1) + + async with self.patch_broker(consume_broker) as br: + await br.start() + + for i in range(5): + await br.publish(i, stream=queue) + + await asyncio.wait( + ( + asyncio.create_task(event.wait()), + asyncio.create_task(event2.wait()), + ), + timeout=3, + ) + + assert mock.call_count == 2, mock.call_count + + async def test_iterator( + self, + queue: str, + mock: MagicMock, + ) -> None: + expected_messages = ("test_message_1", "test_message_2") + + broker = self.get_broker(apply_types=True) + subscriber = broker.subscriber(stream=queue) + + async with self.patch_broker(broker) as br: + await br.start() + + async def publish_test_message() -> None: + await asyncio.sleep(0.1) + for msg in expected_messages: + await br.publish(msg, stream=queue) + + async def consume() -> None: + index_message = 0 + async for msg in subscriber: + result_message = await msg.decode() + + mock(result_message) + + index_message += 1 + if index_message >= len(expected_messages): + break + + await asyncio.wait( + ( + asyncio.create_task(consume()), + asyncio.create_task(publish_test_message()), + ), + timeout=self.timeout, + ) + + calls = [call(msg) for msg in expected_messages] + mock.assert_has_calls(calls=calls) diff --git a/tests/brokers/redis/test_fastapi.py b/tests/brokers/redis/test_fastapi.py index 6ba66374a0..6c4c77fe10 100644 --- a/tests/brokers/redis/test_fastapi.py +++ b/tests/brokers/redis/test_fastapi.py @@ -1,30 +1,27 @@ import asyncio -from typing import List -from unittest.mock import Mock +from unittest.mock import MagicMock import pytest from faststream.redis import ListSub, RedisRouter, StreamSub from faststream.redis.fastapi import RedisRouter as StreamRouter -from faststream.redis.testing import TestRedisBroker, build_message from tests.brokers.base.fastapi import FastAPILocalTestcase, FastAPITestcase +from .basic import RedisMemoryTestcaseConfig -@pytest.mark.redis + +@pytest.mark.redis() class TestRouter(FastAPITestcase): router_class = StreamRouter broker_router_class = RedisRouter - async def test_path( - self, - queue: str, - event: asyncio.Event, - mock: Mock, - ): + async def test_path(self, mock: MagicMock) -> None: + event = asyncio.Event() + router = self.router_class() @router.subscriber("in.{name}") - def subscriber(msg: str, name: str): + def subscriber(msg: str, name: str) -> None: mock(msg=msg, name=name) event.set() @@ -41,27 +38,17 @@ def subscriber(msg: str, name: str): assert event.is_set() mock.assert_called_once_with(msg="hello", name="john") - async def test_connection_params(self, settings): - broker = self.router_class( - host="fake-host", port=6377 - ).broker # kwargs will be ignored - await broker.connect( - host=settings.host, - port=settings.port, - ) - await broker._connection.ping() - await broker.close() - async def test_batch_real( self, - mock: Mock, + mock: MagicMock, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() @router.subscriber(list=ListSub(queue, batch=True, max_records=1)) - async def hello(msg: List[str]): + async def hello(msg: list[str]): event.set() return mock(msg) @@ -78,13 +65,14 @@ async def hello(msg: List[str]): assert event.is_set() mock.assert_called_with(["hi"]) - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_stream( self, - event: asyncio.Event, - mock: Mock, - queue, - ): + mock: MagicMock, + queue: str, + ) -> None: + event = asyncio.Event() + router = self.router_class() @router.subscriber(stream=StreamSub(queue, polling_interval=1000)) @@ -106,17 +94,18 @@ async def handler(msg): mock.assert_called_once_with("hello") - @pytest.mark.slow + @pytest.mark.slow() async def test_consume_stream_batch( self, - event: asyncio.Event, - mock: Mock, - queue, - ): + mock: MagicMock, + queue: str, + ) -> None: + event = asyncio.Event() + router = self.router_class() @router.subscriber(stream=StreamSub(queue, polling_interval=1000, batch=True)) - async def handler(msg: List[str]): + async def handler(msg: list[str]): mock(msg) event.set() @@ -135,29 +124,28 @@ async def handler(msg: List[str]): mock.assert_called_once_with(["hello"]) -class TestRouterLocal(FastAPILocalTestcase): +class TestRouterLocal(RedisMemoryTestcaseConfig, FastAPILocalTestcase): router_class = StreamRouter broker_router_class = RedisRouter - broker_test = staticmethod(TestRedisBroker) - build_message = staticmethod(build_message) async def test_batch_testclient( self, - mock: Mock, + mock: MagicMock, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() @router.subscriber(list=ListSub(queue, batch=True, max_records=1)) - async def hello(msg: List[str]): + async def hello(msg: list[str]): event.set() return mock(msg) - async with TestRedisBroker(router.broker): + async with self.patch_broker(router.broker) as br: await asyncio.wait( ( - asyncio.create_task(router.broker.publish("hi", list=queue)), + asyncio.create_task(br.publish("hi", list=queue)), asyncio.create_task(event.wait()), ), timeout=3, @@ -168,21 +156,22 @@ async def hello(msg: List[str]): async def test_stream_batch_testclient( self, - mock: Mock, + mock: MagicMock, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + router = self.router_class() @router.subscriber(stream=StreamSub(queue, batch=True)) - async def hello(msg: List[str]): + async def hello(msg: list[str]): event.set() return mock(msg) - async with TestRedisBroker(router.broker): + async with self.patch_broker(router.broker) as br: await asyncio.wait( ( - asyncio.create_task(router.broker.publish("hi", stream=queue)), + asyncio.create_task(br.publish("hi", stream=queue)), asyncio.create_task(event.wait()), ), timeout=3, @@ -191,18 +180,17 @@ async def hello(msg: List[str]): assert event.is_set() mock.assert_called_with(["hi"]) - async def test_path(self, queue: str): + async def test_path(self, queue: str) -> None: router = self.router_class() @router.subscriber(queue + ".{name}") async def hello(name): return name - async with self.broker_test(router.broker): - r = await router.broker.publish( + async with self.patch_broker(router.broker) as br: + r = await br.request( "hi", f"{queue}.john", - rpc=True, - rpc_timeout=0.5, + timeout=0.5, ) - assert r == "john" + assert await r.decode() == "john" diff --git a/tests/brokers/redis/test_include_router.py b/tests/brokers/redis/test_include_router.py new file mode 100644 index 0000000000..8d3d44d923 --- /dev/null +++ b/tests/brokers/redis/test_include_router.py @@ -0,0 +1,14 @@ +from tests.brokers.base.include_router import ( + IncludePublisherTestcase, + IncludeSubscriberTestcase, +) + +from .basic import RedisTestcaseConfig + + +class TestSubscriber(RedisTestcaseConfig, IncludeSubscriberTestcase): + pass + + +class TestPublisher(RedisTestcaseConfig, IncludePublisherTestcase): + pass diff --git a/tests/brokers/redis/test_middlewares.py b/tests/brokers/redis/test_middlewares.py index 2c11d0db0c..c75e0914cd 100644 --- a/tests/brokers/redis/test_middlewares.py +++ b/tests/brokers/redis/test_middlewares.py @@ -1,25 +1,23 @@ import pytest -from faststream.redis import RedisBroker, TestRedisBroker from tests.brokers.base.middlewares import ( ExceptionMiddlewareTestcase, MiddlewareTestcase, MiddlewaresOrderTestcase, ) +from .basic import RedisMemoryTestcaseConfig, RedisTestcaseConfig -@pytest.mark.redis -class TestMiddlewares(MiddlewareTestcase): - broker_class = RedisBroker +class TestMiddlewaresOrder(RedisMemoryTestcaseConfig, MiddlewaresOrderTestcase): + pass -@pytest.mark.redis -class TestExceptionMiddlewares(ExceptionMiddlewareTestcase): - broker_class = RedisBroker +@pytest.mark.redis() +class TestMiddlewares(RedisTestcaseConfig, MiddlewareTestcase): + pass -class TestMiddlewaresOrder(MiddlewaresOrderTestcase): - broker_class = RedisBroker - def patch_broker(self, broker: RedisBroker) -> TestRedisBroker: - return TestRedisBroker(broker) +@pytest.mark.redis() +class TestExceptionMiddlewares(RedisTestcaseConfig, ExceptionMiddlewareTestcase): + pass diff --git a/tests/brokers/redis/test_misconfigure.py b/tests/brokers/redis/test_misconfigure.py index 31a64b14b0..bfd8f6e090 100644 --- a/tests/brokers/redis/test_misconfigure.py +++ b/tests/brokers/redis/test_misconfigure.py @@ -1,7 +1,6 @@ import pytest from faststream.exceptions import SetupError -from faststream.kafka import KafkaRouter from faststream.nats import NatsRouter from faststream.redis import RedisBroker, RedisRouter @@ -13,7 +12,7 @@ def test_use_only_redis_router() -> None: with pytest.raises(SetupError): broker.include_router(router) - routers = [RedisRouter(), NatsRouter(), KafkaRouter()] + routers = [RedisRouter(), NatsRouter()] with pytest.raises(SetupError): broker.include_routers(routers) diff --git a/tests/brokers/redis/test_parser.py b/tests/brokers/redis/test_parser.py index c40306adc2..cf16275b65 100644 --- a/tests/brokers/redis/test_parser.py +++ b/tests/brokers/redis/test_parser.py @@ -1,9 +1,10 @@ import pytest -from faststream.redis import RedisBroker from tests.brokers.base.parser import CustomParserTestcase +from .basic import RedisTestcaseConfig -@pytest.mark.redis -class TestCustomParser(CustomParserTestcase): - broker_class = RedisBroker + +@pytest.mark.redis() +class TestCustomParser(RedisTestcaseConfig, CustomParserTestcase): + pass diff --git a/tests/brokers/redis/test_publish.py b/tests/brokers/redis/test_publish.py index e9f22d8b81..919e50f74d 100644 --- a/tests/brokers/redis/test_publish.py +++ b/tests/brokers/redis/test_publish.py @@ -1,37 +1,37 @@ import asyncio +from typing import Any from unittest.mock import MagicMock, patch import pytest from redis.asyncio import Redis from faststream import Context -from faststream.redis import ListSub, RedisBroker, RedisResponse, StreamSub -from faststream.redis.annotations import Pipeline +from faststream.redis import ListSub, Pipeline, RedisResponse, StreamSub from tests.brokers.base.publish import BrokerPublishTestcase from tests.tools import spy_decorator +from .basic import RedisTestcaseConfig -@pytest.mark.redis -@pytest.mark.asyncio -class TestPublish(BrokerPublishTestcase): - def get_broker(self, apply_types: bool = False): - return RedisBroker(apply_types=apply_types) +@pytest.mark.redis() +@pytest.mark.asyncio() +class TestPublish(RedisTestcaseConfig, BrokerPublishTestcase): async def test_list_publisher( self, queue: str, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker() @pub_broker.subscriber(list=queue) @pub_broker.publisher(list=queue + "resp") - async def m(msg): + async def m(msg) -> str: return "" @pub_broker.subscriber(list=queue + "resp") - async def resp(msg): + async def resp(msg) -> None: event.set() mock(msg) @@ -52,13 +52,13 @@ async def resp(msg): async def test_list_publish_batch( self, queue: str, - ): + ) -> None: pub_broker = self.get_broker() msgs_queue = asyncio.Queue(maxsize=2) @pub_broker.subscriber(list=queue) - async def handler(msg): + async def handler(msg) -> None: await msgs_queue.put(msg) async with self.patch_broker(pub_broker) as br: @@ -79,9 +79,10 @@ async def handler(msg): async def test_batch_list_publisher( self, queue: str, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker() batch_list = ListSub(queue + "resp", batch=True) @@ -92,7 +93,7 @@ async def m(msg): return 1, 2, 3 @pub_broker.subscriber(list=batch_list) - async def resp(msg): + async def resp(msg) -> None: event.set() mock(msg) @@ -113,9 +114,10 @@ async def resp(msg): async def test_publisher_with_maxlen( self, queue: str, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker() stream = StreamSub(queue + "resp", maxlen=1) @@ -126,7 +128,7 @@ async def handler(msg): return msg @pub_broker.subscriber(stream=stream) - async def resp(msg): + async def resp(msg) -> None: event.set() mock(msg) @@ -150,18 +152,19 @@ async def resp(msg): async def test_response( self, queue: str, - event: asyncio.Event, mock: MagicMock, - ): + ) -> None: + event = asyncio.Event() + pub_broker = self.get_broker(apply_types=True) @pub_broker.subscriber(list=queue) @pub_broker.publisher(list=queue + "resp") - async def m(): + async def m() -> RedisResponse: return RedisResponse(1, correlation_id="1") @pub_broker.subscriber(list=queue + "resp") - async def resp(msg=Context("message")): + async def resp(msg=Context("message")) -> None: mock( body=msg.body, correlation_id=msg.correlation_id, @@ -185,36 +188,31 @@ async def resp(msg=Context("message")): correlation_id="1", ) - @pytest.mark.asyncio - async def test_response_for_rpc( - self, - queue: str, - event: asyncio.Event, - ): - pub_broker = self.get_broker(apply_types=True) + async def test_response_for_rpc(self, queue: str) -> None: + pub_broker = self.get_broker() @pub_broker.subscriber(queue) - async def handle(): + async def handle(msg: Any) -> RedisResponse: return RedisResponse("Hi!", correlation_id="1") async with self.patch_broker(pub_broker) as br: await br.start() response = await asyncio.wait_for( - br.publish("", queue, rpc=True), + br.request("", queue), timeout=3, ) - assert response == "Hi!", response + assert await response.decode() == "Hi!", response - @pytest.mark.asyncio + @pytest.mark.asyncio() @pytest.mark.parametrize( "type_queue", - [ + ( pytest.param("channel"), pytest.param("list"), pytest.param("stream"), - ], + ), ) async def test_publish_with_pipeline( self, @@ -223,7 +221,6 @@ async def test_publish_with_pipeline( queue: str, mock: MagicMock, ) -> None: - event = asyncio.Event() broker = self.get_broker(apply_types=True) destination = {type_queue: queue + "resp"} @@ -257,7 +254,7 @@ async def resp(msg: str) -> None: assert mock.call_count == 10 - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_publish_batch_with_pipeline( self, event: asyncio.Event, queue: str, mock: MagicMock ) -> None: @@ -283,15 +280,3 @@ async def resp(msgs: list[int]) -> None: await asyncio.wait(tasks, timeout=3) mock.assert_called_once_with([0, 1, 2, 3, 4]) - - @pytest.mark.asyncio - async def test_rpc_with_pipeline_forbidden(self, queue: str) -> None: - pub_broker = self.get_broker(apply_types=True) - - async with self.patch_broker(pub_broker) as br: # noqa: SIM117 - async with br._connection.pipeline() as pipe: - with pytest.raises(RuntimeError, match=r"^You cannot use both"): - await br.publish("", queue, pipeline=pipe, rpc=True) - - with pytest.raises(RuntimeError, match=r"^You cannot use both"): - await br.publisher(queue).publish("", pipeline=pipe, rpc=True) diff --git a/tests/brokers/redis/test_publish_command.py b/tests/brokers/redis/test_publish_command.py new file mode 100644 index 0000000000..d956ffe972 --- /dev/null +++ b/tests/brokers/redis/test_publish_command.py @@ -0,0 +1,14 @@ +from faststream.redis.response import RedisPublishCommand, RedisResponse +from faststream.response import ensure_response +from tests.brokers.base.publish_command import BatchPublishCommandTestcase + + +class TestPublishCommand(BatchPublishCommandTestcase): + publish_command_cls = RedisPublishCommand + + def test_redis_response_class(self) -> None: + response = ensure_response(RedisResponse(body=1, headers={"1": 1}, maxlen=1)) + cmd = self.publish_command_cls.from_cmd(response.as_publish_command()) + assert cmd.body == 1 + assert cmd.headers == {"1": 1} + assert cmd.maxlen == 1 diff --git a/tests/brokers/redis/test_requests.py b/tests/brokers/redis/test_requests.py index e13fe06e92..03c1441fa7 100644 --- a/tests/brokers/redis/test_requests.py +++ b/tests/brokers/redis/test_requests.py @@ -3,9 +3,10 @@ import pytest from faststream import BaseMiddleware -from faststream.redis import RedisBroker, RedisRouter, TestRedisBroker from tests.brokers.base.requests import RequestsTestcase +from .basic import RedisMemoryTestcaseConfig, RedisTestcaseConfig + class Mid(BaseMiddleware): async def on_receive(self) -> None: @@ -14,27 +15,20 @@ async def on_receive(self) -> None: self.msg["data"] = json.dumps(data) async def consume_scope(self, call_next, msg): - msg._decoded_body = msg._decoded_body * 2 + msg.body *= 2 return await call_next(msg) -@pytest.mark.asyncio +@pytest.mark.asyncio() class RedisRequestsTestcase(RequestsTestcase): def get_middleware(self, **kwargs): return Mid - def get_broker(self, **kwargs): - return RedisBroker(**kwargs) - - def get_router(self, **kwargs): - return RedisRouter(**kwargs) - -@pytest.mark.redis -class TestRealRequests(RedisRequestsTestcase): +@pytest.mark.redis() +class TestRealRequests(RedisTestcaseConfig, RedisRequestsTestcase): pass -class TestRequestTestClient(RedisRequestsTestcase): - def patch_broker(self, broker, **kwargs): - return TestRedisBroker(broker, **kwargs) +class TestRequestTestClient(RedisMemoryTestcaseConfig, RedisRequestsTestcase): + pass diff --git a/tests/brokers/redis/test_router.py b/tests/brokers/redis/test_router.py index 33eda512c9..6197645685 100644 --- a/tests/brokers/redis/test_router.py +++ b/tests/brokers/redis/test_router.py @@ -1,135 +1,119 @@ import asyncio +from unittest.mock import MagicMock import pytest from faststream import Path -from faststream.redis import RedisBroker, RedisPublisher, RedisRoute, RedisRouter +from faststream.redis import ( + RedisPublisher, + RedisRoute, + RedisRouter, +) from tests.brokers.base.router import RouterLocalTestcase, RouterTestcase +from .basic import RedisMemoryTestcaseConfig, RedisTestcaseConfig -@pytest.mark.redis -class TestRouter(RouterTestcase): - broker_class = RedisRouter + +@pytest.mark.redis() +class TestRouter(RedisTestcaseConfig, RouterTestcase): route_class = RedisRoute publisher_class = RedisPublisher -class TestRouterLocal(RouterLocalTestcase): - broker_class = RedisRouter +class TestRouterLocal(RedisMemoryTestcaseConfig, RouterLocalTestcase): route_class = RedisRoute publisher_class = RedisPublisher - async def test_router_path( - self, - event, - mock, - router, - pub_broker, - ): + async def test_router_path(self, event: asyncio.Event, mock: MagicMock) -> None: + pub_broker = self.get_broker(apply_types=True) + router = self.get_router() + @router.subscriber("in.{name}.{id}") async def h( name: str = Path(), id: int = Path("id"), - ): + ) -> None: event.set() mock(name=name, id=id) - pub_broker._is_apply_types = True pub_broker.include_router(router) - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() - await pub_broker.publish( - "", - "in.john.2", - rpc=True, - ) + await br.request("", "in.john.2") - assert event.is_set() - mock.assert_called_once_with(name="john", id=2) + assert event.is_set() + mock.assert_called_once_with(name="john", id=2) async def test_router_path_with_prefix( - self, - event, - mock, - router, - pub_broker, - ): - router.prefix = "test." + self, event: asyncio.Event, mock: MagicMock + ) -> None: + pub_broker = self.get_broker(apply_types=True) + + router = self.get_router(prefix="test.") @router.subscriber("in.{name}.{id}") async def h( name: str = Path(), id: int = Path("id"), - ): + ) -> None: event.set() mock(name=name, id=id) - pub_broker._is_apply_types = True pub_broker.include_router(router) - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() - await pub_broker.publish( - "", - "test.in.john.2", - rpc=True, - ) + await br.request("", "test.in.john.2") - assert event.is_set() - mock.assert_called_once_with(name="john", id=2) + assert event.is_set() + mock.assert_called_once_with(name="john", id=2) async def test_router_delay_handler_path( - self, - event, - mock, - router, - pub_broker, - ): + self, event: asyncio.Event, mock: MagicMock + ) -> None: + pub_broker = self.get_broker(apply_types=True) + router = self.get_router() + async def h( name: str = Path(), id: int = Path("id"), - ): + ) -> None: event.set() mock(name=name, id=id) r = type(router)(handlers=(self.route_class(h, channel="in.{name}.{id}"),)) - pub_broker._is_apply_types = True pub_broker.include_router(r) - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() - await pub_broker.publish( - "", - "in.john.2", - rpc=True, - ) + await br.request("", "in.john.2") - assert event.is_set() - mock.assert_called_once_with(name="john", id=2) + assert event.is_set() + mock.assert_called_once_with(name="john", id=2) - async def test_delayed_channel_handlers( - self, - event: asyncio.Event, - queue: str, - pub_broker: RedisBroker, - ): - def response(m): + async def test_delayed_channel_handlers(self, queue: str) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + + def response(m) -> None: event.set() r = RedisRouter(prefix="test_", handlers=(RedisRoute(response, channel=queue),)) pub_broker.include_router(r) - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task( - pub_broker.publish("hello", channel=f"test_{queue}") - ), + asyncio.create_task(br.publish("hello", channel=f"test_{queue}")), asyncio.create_task(event.wait()), ), timeout=3, @@ -137,27 +121,24 @@ def response(m): assert event.is_set() - async def test_delayed_list_handlers( - self, - event: asyncio.Event, - queue: str, - pub_broker: RedisBroker, - ): - def response(m): + async def test_delayed_list_handlers(self, queue: str) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + + def response(m) -> None: event.set() r = RedisRouter(prefix="test_", handlers=(RedisRoute(response, list=queue),)) pub_broker.include_router(r) - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task( - pub_broker.publish("hello", list=f"test_{queue}") - ), + asyncio.create_task(br.publish("hello", list=f"test_{queue}")), asyncio.create_task(event.wait()), ), timeout=3, @@ -165,27 +146,24 @@ def response(m): assert event.is_set() - async def test_delayed_stream_handlers( - self, - event: asyncio.Event, - queue: str, - pub_broker: RedisBroker, - ): - def response(m): + async def test_delayed_stream_handlers(self, queue: str) -> None: + event = asyncio.Event() + + pub_broker = self.get_broker() + + def response(m) -> None: event.set() r = RedisRouter(prefix="test_", handlers=(RedisRoute(response, stream=queue),)) pub_broker.include_router(r) - async with pub_broker: - await pub_broker.start() + async with self.patch_broker(pub_broker) as br: + await br.start() await asyncio.wait( ( - asyncio.create_task( - pub_broker.publish("hello", stream=f"test_{queue}") - ), + asyncio.create_task(br.publish("hello", stream=f"test_{queue}")), asyncio.create_task(event.wait()), ), timeout=3, diff --git a/tests/brokers/redis/test_rpc.py b/tests/brokers/redis/test_rpc.py deleted file mode 100644 index 6895c7c998..0000000000 --- a/tests/brokers/redis/test_rpc.py +++ /dev/null @@ -1,25 +0,0 @@ -import pytest - -from faststream.redis import RedisBroker -from tests.brokers.base.rpc import BrokerRPCTestcase, ReplyAndConsumeForbidden - - -@pytest.mark.redis -class TestRPC(BrokerRPCTestcase, ReplyAndConsumeForbidden): - def get_broker(self, apply_types: bool = False): - return RedisBroker(apply_types=apply_types) - - @pytest.mark.asyncio - async def test_list_rpc(self, queue: str): - rpc_broker = self.get_broker() - - @rpc_broker.subscriber(list=queue) - async def m(m): # pragma: no cover - return "1" - - async with self.patch_broker(rpc_broker) as br: - await br.start() - - r = await br.publish("hello", list=queue, rpc_timeout=3, rpc=True) - - assert r == "1" diff --git a/tests/brokers/redis/test_schemas.py b/tests/brokers/redis/test_schemas.py index 8c0fea9186..fa85d64f92 100644 --- a/tests/brokers/redis/test_schemas.py +++ b/tests/brokers/redis/test_schemas.py @@ -3,7 +3,7 @@ from faststream.redis import StreamSub -def test_stream_group(): +def test_stream_group() -> None: with pytest.raises(ValueError): # noqa: PT011 StreamSub("test", group="group") diff --git a/tests/brokers/redis/test_test_client.py b/tests/brokers/redis/test_test_client.py index 1203c9448a..da352a0c7b 100644 --- a/tests/brokers/redis/test_test_client.py +++ b/tests/brokers/redis/test_test_client.py @@ -3,48 +3,29 @@ import pytest from faststream import BaseMiddleware -from faststream.exceptions import SetupError -from faststream.redis import ListSub, RedisBroker, StreamSub, TestRedisBroker +from faststream.redis import ListSub, StreamSub from faststream.redis.testing import FakeProducer from tests.brokers.base.testclient import BrokerTestclientTestcase +from .basic import RedisMemoryTestcaseConfig -@pytest.mark.asyncio -class TestTestclient(BrokerTestclientTestcase): - test_class = TestRedisBroker - def get_broker(self, apply_types: bool = False) -> RedisBroker: - return RedisBroker(apply_types=apply_types) - - def patch_broker(self, broker: RedisBroker) -> TestRedisBroker: - return TestRedisBroker(broker) - - def get_fake_producer_class(self) -> type: - return FakeProducer - - async def test_rpc_conflicts_reply(self, queue): - async with TestRedisBroker(RedisBroker()) as br: - with pytest.raises(SetupError): - await br.publish( - "", - queue, - rpc=True, - reply_to="response", - ) - - @pytest.mark.redis +@pytest.mark.asyncio() +class TestTestclient(RedisMemoryTestcaseConfig, BrokerTestclientTestcase): + @pytest.mark.redis() async def test_with_real_testclient( self, queue: str, - event: asyncio.Event, - ): + ) -> None: + event = asyncio.Event() + broker = self.get_broker() @broker.subscriber(queue) - def subscriber(m): + def subscriber(m) -> None: event.set() - async with TestRedisBroker(broker, with_real=True) as br: + async with self.patch_broker(broker, with_real=True) as br: await asyncio.wait( ( asyncio.create_task(br.publish("hello", queue)), @@ -55,7 +36,7 @@ def subscriber(m): assert event.is_set() - async def test_respect_middleware(self, queue): + async def test_respect_middleware(self, queue: str) -> None: routes = [] class Middleware(BaseMiddleware): @@ -63,22 +44,22 @@ async def on_receive(self) -> None: routes.append(None) return await super().on_receive() - broker = RedisBroker(middlewares=(Middleware,)) + broker = self.get_broker(middlewares=(Middleware,)) @broker.subscriber(queue) - async def h1(): ... + async def h1(m) -> None: ... @broker.subscriber(queue + "1") - async def h2(): ... + async def h2(m) -> None: ... - async with TestRedisBroker(broker) as br: + async with self.patch_broker(broker) as br: await br.publish("", queue) await br.publish("", queue + "1") assert len(routes) == 2 - @pytest.mark.redis - async def test_real_respect_middleware(self, queue): + @pytest.mark.redis() + async def test_real_respect_middleware(self, queue: str) -> None: routes = [] class Middleware(BaseMiddleware): @@ -86,15 +67,15 @@ async def on_receive(self) -> None: routes.append(None) return await super().on_receive() - broker = RedisBroker(middlewares=(Middleware,)) + broker = self.get_broker(middlewares=(Middleware,)) @broker.subscriber(queue) - async def h1(): ... + async def h1(m) -> None: ... @broker.subscriber(queue + "1") - async def h2(): ... + async def h2(m) -> None: ... - async with TestRedisBroker(broker, with_real=True) as br: + async with self.patch_broker(broker, with_real=True) as br: await br.publish("", queue) await br.publish("", queue + "1") await h1.wait_call(3) @@ -102,7 +83,7 @@ async def h2(): ... assert len(routes) == 2 - async def test_pub_sub_pattern(self): + async def test_pub_sub_pattern(self) -> None: broker = self.get_broker() @broker.subscriber("test.{name}") @@ -110,13 +91,13 @@ async def handler(msg): return msg async with self.patch_broker(broker) as br: - assert await br.publish(1, "test.name.useless", rpc=True) == 1 + assert await (await br.request(1, "test.name.useless")).decode() == 1 handler.mock.assert_called_once_with(1) async def test_list( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(list=queue) @@ -124,17 +105,17 @@ async def handler(msg): return msg async with self.patch_broker(broker) as br: - assert await br.publish(1, list=queue, rpc=True) == 1 + assert await (await br.request(1, list=queue)).decode() == 1 handler.mock.assert_called_once_with(1) async def test_batch_pub_by_default_pub( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(list=ListSub(queue, batch=True)) - async def m(msg): + async def m(msg) -> None: pass async with self.patch_broker(broker) as br: @@ -144,11 +125,11 @@ async def m(msg): async def test_batch_pub_by_pub_batch( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(list=ListSub(queue, batch=True)) - async def m(msg): + async def m(msg) -> None: pass async with self.patch_broker(broker) as br: @@ -158,7 +139,7 @@ async def m(msg): async def test_batch_publisher_mock( self, queue: str, - ): + ) -> None: broker = self.get_broker() batch_list = ListSub(queue + "1", batch=True) @@ -177,7 +158,7 @@ async def m(msg): async def test_stream( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(stream=queue) @@ -185,17 +166,17 @@ async def handler(msg): return msg async with self.patch_broker(broker) as br: - assert await br.publish(1, stream=queue, rpc=True) == 1 + assert await (await br.request(1, stream=queue)).decode() == 1 handler.mock.assert_called_once_with(1) async def test_stream_batch_pub_by_default_pub( self, queue: str, - ): + ) -> None: broker = self.get_broker() @broker.subscriber(stream=StreamSub(queue, batch=True)) - async def m(msg): + async def m(msg) -> None: pass async with self.patch_broker(broker) as br: @@ -205,7 +186,7 @@ async def m(msg): async def test_stream_publisher( self, queue: str, - ): + ) -> None: broker = self.get_broker() batch_stream = StreamSub(queue + "1") @@ -221,26 +202,24 @@ async def m(msg): m.mock.assert_called_once_with("hello") publisher.mock.assert_called_once_with([1, 2, 3]) - async def test_publish_to_none( - self, - queue: str, - ): + async def test_publish_to_none(self) -> None: broker = self.get_broker() async with self.patch_broker(broker) as br: with pytest.raises(ValueError): # noqa: PT011 await br.publish("hello") - @pytest.mark.redis - async def test_broker_gets_patched_attrs_within_cm(self): - await super().test_broker_gets_patched_attrs_within_cm() + @pytest.mark.redis() + async def test_broker_gets_patched_attrs_within_cm(self) -> None: + await super().test_broker_gets_patched_attrs_within_cm(FakeProducer) - @pytest.mark.redis - async def test_broker_with_real_doesnt_get_patched(self): + @pytest.mark.redis() + async def test_broker_with_real_doesnt_get_patched(self) -> None: await super().test_broker_with_real_doesnt_get_patched() - @pytest.mark.redis + @pytest.mark.redis() async def test_broker_with_real_patches_publishers_and_subscribers( - self, queue: str - ): + self, + queue: str, + ) -> None: await super().test_broker_with_real_patches_publishers_and_subscribers(queue) diff --git a/tests/brokers/test_pushback.py b/tests/brokers/test_pushback.py deleted file mode 100644 index ac56078cb0..0000000000 --- a/tests/brokers/test_pushback.py +++ /dev/null @@ -1,122 +0,0 @@ -from unittest.mock import AsyncMock - -import pytest - -from faststream.broker.acknowledgement_watcher import ( - CounterWatcher, - EndlessWatcher, - WatcherContext, -) -from faststream.exceptions import NackMessage, SkipMessage - - -@pytest.fixture -def message(): - return AsyncMock(message_id=1, committed=None) - - -@pytest.mark.asyncio -async def test_push_back_correct(async_mock: AsyncMock, message): - watcher = CounterWatcher(3) - - context = WatcherContext( - message=message, - watcher=watcher, - ) - - async with context: - await async_mock() - - async_mock.assert_awaited_once() - message.ack.assert_awaited_once() - assert not watcher.memory.get(message.message_id) - - -@pytest.mark.asyncio -async def test_push_back_endless_correct(async_mock: AsyncMock, message): - watcher = EndlessWatcher() - - context = WatcherContext( - message=message, - watcher=watcher, - ) - - async with context: - await async_mock() - - async_mock.assert_awaited_once() - message.ack.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_push_back_watcher(async_mock: AsyncMock, message): - watcher = CounterWatcher(3) - - context = WatcherContext( - message=message, - watcher=watcher, - ) - - async_mock.side_effect = ValueError("Ooops!") - - while not message.reject.called: - with pytest.raises(ValueError): # noqa: PT011 - async with context: - await async_mock() - - assert not message.ack.await_count - assert message.nack.await_count == 3 - message.reject.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_push_endless_back_watcher(async_mock: AsyncMock, message): - watcher = EndlessWatcher() - - context = WatcherContext( - message=message, - watcher=watcher, - ) - - async_mock.side_effect = ValueError("Ooops!") - - while message.nack.await_count < 10: - with pytest.raises(ValueError): # noqa: PT011 - async with context: - await async_mock() - - assert not message.ack.called - assert not message.reject.called - assert message.nack.await_count == 10 - - -@pytest.mark.asyncio -async def test_ignore_skip(async_mock: AsyncMock, message): - watcher = CounterWatcher(3) - - context = WatcherContext( - message=message, - watcher=watcher, - ) - - async with context: - raise SkipMessage() - - assert not message.nack.called - assert not message.reject.called - assert not message.ack.called - - -@pytest.mark.asyncio -async def test_additional_params_with_handler_exception(async_mock: AsyncMock, message): - watcher = EndlessWatcher() - - context = WatcherContext( - message=message, - watcher=watcher, - ) - - async with context: - raise NackMessage(delay=5) - - message.nack.assert_called_with(delay=5) diff --git a/tests/brokers/test_response.py b/tests/brokers/test_response.py index 785e0a21cf..a8b669cc52 100644 --- a/tests/brokers/test_response.py +++ b/tests/brokers/test_response.py @@ -1,25 +1,26 @@ -from faststream.broker.response import Response, ensure_response +from faststream.response import ensure_response +from faststream.response.response import Response -def test_raw_data(): +def test_raw_data() -> None: resp = ensure_response(1) assert resp.body == 1 assert resp.headers == {} -def test_response_with_response_instance(): +def test_response_with_response_instance() -> None: resp = ensure_response(Response(1, headers={"some": 1})) assert resp.body == 1 assert resp.headers == {"some": 1} -def test_headers_override(): - resp = Response(1, headers={"some": 1}) - resp.add_headers({"some": 2}) - assert resp.headers == {"some": 2} +def test_add_headers_not_overrides() -> None: + publish_cmd = Response(1, headers={1: 1, 2: 2}).as_publish_command() + publish_cmd.add_headers({1: "ignored", 3: 3}, override=False) + assert publish_cmd.headers == {1: 1, 2: 2, 3: 3} -def test_headers_with_default(): - resp = Response(1, headers={"some": 1}) - resp.add_headers({"some": 2}, override=False) - assert resp.headers == {"some": 1} +def test_add_headers_overrides() -> None: + publish_cmd = Response(1, headers={1: "ignored", 2: 2}).as_publish_command() + publish_cmd.add_headers({1: 1, 3: 3}, override=True) + assert publish_cmd.headers == {1: 1, 2: 2, 3: 3} diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index 95b0fa19c4..5025499339 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -1,28 +1,22 @@ import os +import select +import signal import subprocess import threading import time -from contextlib import contextmanager +from collections.abc import Generator +from contextlib import AbstractContextManager, contextmanager, suppress +from pathlib import Path from textwrap import dedent -from typing import ( - TYPE_CHECKING, - ContextManager, - Dict, - Generator, - List, - Optional, - Protocol, -) +from typing import Protocol import pytest from faststream import FastStream +from faststream._internal._compat import IS_WINDOWS -if TYPE_CHECKING: - from pathlib import Path - -@pytest.fixture +@pytest.fixture() def broker(): # separate import from e2e tests from faststream.rabbit import RabbitBroker @@ -30,22 +24,22 @@ def broker(): return RabbitBroker() -@pytest.fixture -def app_without_logger(broker): - return FastStream(broker, None) +@pytest.fixture() +def app_without_logger(broker) -> FastStream: + return FastStream(broker, logger=None) -@pytest.fixture -def app_without_broker(): +@pytest.fixture() +def app_without_broker() -> FastStream: return FastStream() -@pytest.fixture -def app(broker): +@pytest.fixture() +def app(broker) -> FastStream: return FastStream(broker) -@pytest.fixture +@pytest.fixture() def faststream_tmp_path(tmp_path: "Path"): faststream_tmp = tmp_path / "faststream_templates" faststream_tmp.mkdir(exist_ok=True) @@ -55,10 +49,10 @@ def faststream_tmp_path(tmp_path: "Path"): class GenerateTemplateFactory(Protocol): def __call__( self, code: str, filename: str = "temp_app.py" - ) -> ContextManager["Path"]: ... + ) -> AbstractContextManager[Path]: ... -@pytest.fixture +@pytest.fixture() def generate_template( faststream_tmp_path: "Path", ) -> GenerateTemplateFactory: @@ -79,73 +73,137 @@ def factory( return factory -class CliThread(Protocol): - process: Optional[subprocess.Popen] +class CLIThread: + def __init__( + self, + command: tuple[str, ...], + env: dict[str, str], + ) -> None: + self.process = subprocess.Popen( + command, + stderr=subprocess.PIPE, + text=True, + shell=False, + env=env, + ) + self.running = True + self.started = False + + self.stderr = "" + + self.__std_poll_thread = threading.Thread(target=self._poll_std) + self.__std_poll_thread.start() + + def _poll_std(self) -> None: + assert self.process.stderr + + if IS_WINDOWS: + return + + while self.running: + rlist, _, _ = select.select([self.process.stderr], [], [], 0.1) + if rlist: + self.started = True + + if line := self.process.stderr.readline(): + self.stderr += line.strip() + + else: + break + + elif self.process.poll() is not None: + break + + def wait_for_stderr(self, message: str, timeout: float = 2.0) -> bool: + assert self.process.stderr + + expiration_time = time.time() + timeout + + while time.time() < expiration_time: + time.sleep(0.1) + if message in self.stderr: + return True + + if self.process.returncode is not None: + return message in self.process.stderr.read() + + return False + + def wait(self, timeout: float) -> None: + self.process.wait(timeout) - def stop(self) -> None: ... + def signint(self) -> None: + if IS_WINDOWS: + self.process.terminate() + else: + self.process.send_signal(signal.SIGINT) + + def stop(self) -> None: + self.process.terminate() + + self.running = False + with suppress(Exception): + self.__std_poll_thread.join() + + try: + self.wait(5) + + except subprocess.TimeoutExpired: + self.process.kill() class FastStreamCLIFactory(Protocol): def __call__( self, - cmd: List[str], - wait_time: float = 1.5, - extra_env: Optional[Dict[str, str]] = None, - ) -> ContextManager[CliThread]: ... + *cmd: str, + wait_time: float = 2.0, + extra_env: dict[str, str] | None = None, + ) -> AbstractContextManager[CLIThread]: ... -@pytest.fixture -def faststream_cli(faststream_tmp_path: "Path") -> FastStreamCLIFactory: +@pytest.fixture() +def faststream_cli(faststream_tmp_path: Path) -> FastStreamCLIFactory: @contextmanager - def factory( - cmd: List[str], + def cli_factory( + *cmd: str, wait_time: float = 2.0, - extra_env: Optional[Dict[str, str]] = None, - ) -> Generator[CliThread, None, None]: - class RealCLIThread(threading.Thread): - def __init__(self, command: List[str], env: Dict[str, str]): - super().__init__() - self.command = command - self.process: Optional[subprocess.Popen] = None - self.env = env - - def run(self) -> None: - self.process = subprocess.Popen( - self.command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - shell=False, - env=self.env, - ) - self.process.wait() - - def stop(self) -> None: - if self.process: - self.process.terminate() - try: - self.process.wait(timeout=5) - except subprocess.TimeoutExpired: - self.process.kill() - - extra_env = extra_env or {} - env = os.environ.copy() - if extra_env: - env.update(**extra_env) - env.update( - **{ + extra_env: dict[str, str] | None = None, + ) -> Generator[CLIThread, None, None]: + env = ( + os.environ.copy() + | { "PATH": f"{faststream_tmp_path}:{os.environ['PATH']}", "PYTHONPATH": str(faststream_tmp_path), } + | (extra_env or {}) ) - cli = RealCLIThread(cmd, env) - cli.start() - time.sleep(wait_time) + + cli = CLIThread(cmd, env) + + wait_for_startup(cli, wait_time) try: yield cli finally: cli.stop() - cli.join() - return factory + return cli_factory + + +def wait_for_startup( + cli: CLIThread, + timeout: float = 10, + check_interval: float = 0.1, +) -> None: + start_time = time.time() + + while time.time() - start_time < timeout: + if cli.started: + return + + if cli.process.poll() is not None: + return + + time.sleep(check_interval) + + return diff --git a/tests/cli/rabbit/test_app.py b/tests/cli/rabbit/test_app.py deleted file mode 100644 index 5db19a9ed5..0000000000 --- a/tests/cli/rabbit/test_app.py +++ /dev/null @@ -1,410 +0,0 @@ -import logging -import os -import signal -from contextlib import asynccontextmanager -from unittest.mock import AsyncMock, Mock, patch - -import anyio -import pytest - -from faststream import FastStream, TestApp -from faststream._compat import IS_WINDOWS -from faststream.log import logger -from faststream.rabbit.testing import TestRabbitBroker - - -def test_init(app: FastStream, broker): - assert app.broker is broker - assert app.logger is logger - - -def test_init_without_broker(app_without_broker: FastStream): - assert app_without_broker.broker is None - - -def test_init_without_logger(app_without_logger: FastStream): - assert app_without_logger.logger is None - - -def test_set_broker(broker, app_without_broker: FastStream): - assert app_without_broker.broker is None - app_without_broker.set_broker(broker) - assert app_without_broker.broker is broker - - -@pytest.mark.asyncio -async def test_set_broker_in_on_startup_hook(app_without_broker: FastStream, broker): - def add_broker(): - app_without_broker.set_broker(broker) - - app_without_broker.on_startup(add_broker) - - async with TestRabbitBroker(broker): - await app_without_broker._startup() - - -@pytest.mark.asyncio -async def test_startup_fails_if_no_broker_was_provided(app_without_broker: FastStream): - with pytest.raises(AssertionError): - await app_without_broker._startup() - - -def test_log(app: FastStream, app_without_logger: FastStream): - app._log(logging.INFO, "test") - app_without_logger._log(logging.INFO, "test") - - -@pytest.mark.asyncio -async def test_on_startup_calls(async_mock: AsyncMock, mock: Mock): - def call1(): - mock.call_start1() - assert not async_mock.call_start2.called - - async def call2(): - await async_mock.call_start2() - assert mock.call_start1.call_count == 1 - - test_app = FastStream(on_startup=[call1, call2]) - - await test_app.start() - - mock.call_start1.assert_called_once() - async_mock.call_start2.assert_called_once() - - -@pytest.mark.asyncio -async def test_startup_calls_lifespans(mock: Mock, app_without_broker: FastStream): - def call1(): - mock.call_start1() - assert not mock.call_start2.called - - def call2(): - mock.call_start2() - assert mock.call_start1.call_count == 1 - - app_without_broker.on_startup(call1) - app_without_broker.on_startup(call2) - - await app_without_broker.start() - - mock.call_start1.assert_called_once() - mock.call_start2.assert_called_once() - - -@pytest.mark.asyncio -async def test_on_shutdown_calls(async_mock: AsyncMock, mock: Mock): - def call1(): - mock.call_stop1() - assert not async_mock.call_stop2.called - - async def call2(): - await async_mock.call_stop2() - assert mock.call_stop1.call_count == 1 - - test_app = FastStream(on_shutdown=[call1, call2]) - - await test_app.stop() - - mock.call_stop1.assert_called_once() - async_mock.call_stop2.assert_called_once() - - -@pytest.mark.asyncio -async def test_shutdown_calls_lifespans(mock: Mock, app_without_broker: FastStream): - def call1(): - mock.call_stop1() - assert not mock.call_stop2.called - - def call2(): - mock.call_stop2() - assert mock.call_stop1.call_count == 1 - - app_without_broker.on_shutdown(call1) - app_without_broker.on_shutdown(call2) - - await app_without_broker.stop() - - mock.call_stop1.assert_called_once() - mock.call_stop2.assert_called_once() - - -@pytest.mark.asyncio -async def test_after_startup_calls(async_mock: AsyncMock, mock: Mock, broker): - def call1(): - mock.after_startup1() - assert not async_mock.after_startup2.called - - async def call2(): - await async_mock.after_startup2() - assert mock.after_startup1.call_count == 1 - - test_app = FastStream(broker=broker, after_startup=[call1, call2]) - - with patch.object(test_app.broker, "start", async_mock.broker_start): - await test_app.start() - - mock.after_startup1.assert_called_once() - async_mock.after_startup2.assert_called_once() - - -@pytest.mark.asyncio -async def test_startup_lifespan_before_broker_started(async_mock, app: FastStream): - @app.on_startup - async def call(): - await async_mock.before() - assert not async_mock.broker_start.called - - @app.after_startup - async def call_after(): - await async_mock.after() - async_mock.before.assert_awaited_once() - async_mock.broker_start.assert_called_once() - - with patch.object(app.broker, "start", async_mock.broker_start): - await app.start() - - async_mock.broker_start.assert_called_once() - async_mock.after.assert_awaited_once() - async_mock.before.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_after_shutdown_calls(async_mock: AsyncMock, mock: Mock, broker): - def call1(): - mock.after_shutdown1() - assert not async_mock.after_shutdown2.called - - async def call2(): - await async_mock.after_shutdown2() - assert mock.after_shutdown1.call_count == 1 - - test_app = FastStream(broker=broker, after_shutdown=[call1, call2]) - - with patch.object(test_app.broker, "start", async_mock.broker_start): - await test_app.stop() - - mock.after_shutdown1.assert_called_once() - async_mock.after_shutdown2.assert_called_once() - - -@pytest.mark.asyncio -async def test_shutdown_lifespan_after_broker_stopped( - mock, async_mock, app: FastStream -): - @app.after_shutdown - async def call(): - await async_mock.after() - async_mock.broker_stop.assert_called_once() - - @app.on_shutdown - async def call_before(): - await async_mock.before() - assert not async_mock.broker_stop.called - - with patch.object(app.broker, "close", async_mock.broker_stop): - await app.stop() - - async_mock.broker_stop.assert_called_once() - async_mock.after.assert_awaited_once() - async_mock.before.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_running(async_mock, app: FastStream): - app.exit() - - with patch.object(app.broker, "start", async_mock.broker_run), patch.object( - app.broker, "close", async_mock.broker_stopped - ): - await app.run() - - async_mock.broker_run.assert_called_once() - async_mock.broker_stopped.assert_called_once() - - -@pytest.mark.asyncio -async def test_exception_group(async_mock: AsyncMock, app: FastStream): - async_mock.side_effect = ValueError("Ooops!") - - @app.on_startup - async def f(): - await async_mock() - - with pytest.raises(ValueError, match="Ooops!"): - await app.run() - - -@pytest.mark.asyncio -async def test_running_lifespan_contextmanager(async_mock, mock: Mock, app: FastStream): - @asynccontextmanager - async def lifespan(env: str): - mock.on(env) - yield - mock.off() - - app = FastStream(app.broker, lifespan=lifespan) - app.exit() - - with patch.object(app.broker, "start", async_mock.broker_run), patch.object( - app.broker, "close", async_mock.broker_stopped - ): - await app.run(run_extra_options={"env": "test"}) - - async_mock.broker_run.assert_called_once() - async_mock.broker_stopped.assert_called_once() - - mock.on.assert_called_once_with("test") - mock.off.assert_called_once() - - -@pytest.mark.asyncio -async def test_test_app(mock: Mock): - app = FastStream() - - app.on_startup(mock.on) - app.on_shutdown(mock.off) - - async with TestApp(app): - pass - - mock.on.assert_called_once() - mock.off.assert_called_once() - - -@pytest.mark.asyncio -async def test_test_app_with_excp(mock: Mock): - app = FastStream() - - app.on_startup(mock.on) - app.on_shutdown(mock.off) - - with pytest.raises(ValueError): # noqa: PT011 - async with TestApp(app): - raise ValueError() - - mock.on.assert_called_once() - mock.off.assert_called_once() - - -def test_sync_test_app(mock: Mock): - app = FastStream() - - app.on_startup(mock.on) - app.on_shutdown(mock.off) - - with TestApp(app): - pass - - mock.on.assert_called_once() - mock.off.assert_called_once() - - -def test_sync_test_app_with_excp(mock: Mock): - app = FastStream() - - app.on_startup(mock.on) - app.on_shutdown(mock.off) - - with pytest.raises(ValueError), TestApp(app): # noqa: PT011 - raise ValueError() - - mock.on.assert_called_once() - mock.off.assert_called_once() - - -@pytest.mark.asyncio -async def test_lifespan_contextmanager(async_mock: AsyncMock, app: FastStream): - @asynccontextmanager - async def lifespan(env: str): - await async_mock.on(env) - yield - await async_mock.off() - - app = FastStream(app.broker, lifespan=lifespan) - - with patch.object(app.broker, "start", async_mock.broker_run), patch.object( - app.broker, "close", async_mock.broker_stopped - ): - async with TestApp(app, {"env": "test"}): - pass - - async_mock.on.assert_awaited_once_with("test") - async_mock.off.assert_awaited_once() - async_mock.broker_run.assert_called_once() - async_mock.broker_stopped.assert_called_once() - - -def test_sync_lifespan_contextmanager(async_mock: AsyncMock, app: FastStream): - @asynccontextmanager - async def lifespan(env: str): - await async_mock.on(env) - yield - await async_mock.off() - - app = FastStream(app.broker, lifespan=lifespan) - - with patch.object(app.broker, "start", async_mock.broker_run), patch.object( - app.broker, "close", async_mock.broker_stopped - ), TestApp(app, {"env": "test"}): - pass - - async_mock.on.assert_awaited_once_with("test") - async_mock.off.assert_awaited_once() - async_mock.broker_run.assert_called_once() - async_mock.broker_stopped.assert_called_once() - - -@pytest.mark.asyncio -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") -async def test_stop_with_sigint(async_mock, app: FastStream): - with patch.object(app.broker, "start", async_mock.broker_run_sigint), patch.object( - app.broker, "close", async_mock.broker_stopped_sigint - ): - async with anyio.create_task_group() as tg: - tg.start_soon(app.run) - tg.start_soon(_kill, signal.SIGINT) - - async_mock.broker_run_sigint.assert_called_once() - async_mock.broker_stopped_sigint.assert_called_once() - - -@pytest.mark.asyncio -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") -async def test_stop_with_sigterm(async_mock, app: FastStream): - with patch.object(app.broker, "start", async_mock.broker_run_sigterm), patch.object( - app.broker, "close", async_mock.broker_stopped_sigterm - ): - async with anyio.create_task_group() as tg: - tg.start_soon(app.run) - tg.start_soon(_kill, signal.SIGTERM) - - async_mock.broker_run_sigterm.assert_called_once() - async_mock.broker_stopped_sigterm.assert_called_once() - - -@pytest.mark.asyncio -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") -async def test_run_asgi(async_mock: AsyncMock, app: FastStream): - asgi_routes = [("/", lambda scope, receive, send: None)] - asgi_app = app.as_asgi(asgi_routes=asgi_routes) - assert asgi_app.broker is app.broker - assert asgi_app.logger is app.logger - assert asgi_app.lifespan_context is app.lifespan_context - assert asgi_app._on_startup_calling is app._on_startup_calling - assert asgi_app._after_startup_calling is app._after_startup_calling - assert asgi_app._on_shutdown_calling is app._on_shutdown_calling - assert asgi_app._after_shutdown_calling is app._after_shutdown_calling - assert asgi_app.routes == asgi_routes - - with patch.object(app.broker, "start", async_mock.broker_run), patch.object( - app.broker, "close", async_mock.broker_stopped - ): - async with anyio.create_task_group() as tg: - tg.start_soon(app.run) - tg.start_soon(_kill, signal.SIGINT) - - async_mock.broker_run.assert_called_once() - - -async def _kill(sig): - os.kill(os.getpid(), sig) diff --git a/tests/cli/rabbit/test_logs.py b/tests/cli/rabbit/test_logs.py deleted file mode 100644 index 79e140da99..0000000000 --- a/tests/cli/rabbit/test_logs.py +++ /dev/null @@ -1,60 +0,0 @@ -import logging - -import pytest - -from faststream import FastStream -from faststream.cli.utils.logs import LogLevels, get_log_level, set_log_level -from faststream.rabbit import RabbitBroker - - -@pytest.mark.parametrize( - "level", - ( # noqa: PT007 - pytest.param(logging.ERROR, id=str(logging.ERROR)), - *(pytest.param(level, id=level) for level in LogLevels.__members__), - *( - pytest.param(level, id=str(level)) - for level in LogLevels.__members__.values() - ), - ), -) -def test_set_level(level, app: FastStream): - level = get_log_level(level) - set_log_level(level, app) - assert app.logger.level is app.broker.logger.level is level - - -@pytest.mark.parametrize( - ("level", "broker"), - ( # noqa: PT007 - pytest.param( - logging.CRITICAL, - FastStream(), - id="empty app", - ), - pytest.param( - logging.CRITICAL, - FastStream(RabbitBroker(), logger=None), - id="app without logger", - ), - pytest.param( - logging.CRITICAL, - FastStream(RabbitBroker(logger=None)), - id="broker without logger", - ), - pytest.param( - logging.CRITICAL, - FastStream(RabbitBroker(logger=None), logger=None), - id="both without logger", - ), - ), -) -def test_set_level_to_none(level, app: FastStream): - set_log_level(get_log_level(level), app) - - -def test_set_default(): - app = FastStream() - level = "wrong_level" - set_log_level(get_log_level(level), app) - assert app.logger.level is logging.INFO diff --git a/tests/cli/rabbit/__init__.py b/tests/cli/require_broker/__init__.py similarity index 100% rename from tests/cli/rabbit/__init__.py rename to tests/cli/require_broker/__init__.py diff --git a/tests/cli/require_broker/test_app.py b/tests/cli/require_broker/test_app.py new file mode 100644 index 0000000000..d7032c6c3c --- /dev/null +++ b/tests/cli/require_broker/test_app.py @@ -0,0 +1,465 @@ +import logging +import os +import signal +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from unittest.mock import AsyncMock, MagicMock, patch + +import anyio +import pytest + +from faststream import FastStream, TestApp +from faststream._internal.logger import logger +from faststream.asgi import AsgiResponse +from faststream.rabbit import RabbitBroker, TestRabbitBroker +from tests.marks import skip_windows + + +def test_init(app: FastStream, broker: RabbitBroker) -> None: + assert app.broker is broker + assert app.logger is logger + + +def test_init_without_broker(app_without_broker: FastStream) -> None: + assert app_without_broker.broker is None + + +def test_init_without_logger(app_without_logger: FastStream) -> None: + assert app_without_logger.logger is None + + +def test_set_broker(broker: RabbitBroker, app_without_broker: FastStream) -> None: + assert app_without_broker.broker is None + app_without_broker.set_broker(broker) + assert app_without_broker.broker is broker + + +@pytest.mark.asyncio() +async def test_set_broker_in_on_startup_hook( + app_without_broker: FastStream, broker: RabbitBroker +) -> None: + def add_broker() -> None: + app_without_broker.set_broker(broker) + + app_without_broker.on_startup(add_broker) + + async with TestRabbitBroker(broker): + await app_without_broker._startup() + + +@pytest.mark.asyncio() +async def test_startup_fails_if_no_broker_was_provided( + app_without_broker: FastStream, +) -> None: + with pytest.raises(AssertionError): + await app_without_broker._startup() + + +def test_log(app: FastStream, app_without_logger: FastStream) -> None: + app._log(logging.INFO, "test") + app_without_logger._log(logging.INFO, "test") + + +@pytest.mark.asyncio() +async def test_on_startup_calls(async_mock: AsyncMock, mock: MagicMock) -> None: + def call1() -> None: + mock.call_start1() + assert not async_mock.call_start2.called + + async def call2() -> None: + await async_mock.call_start2() + assert mock.call_start1.call_count == 1 + + test_app = FastStream(AsyncMock(), on_startup=[call1, call2]) + + await test_app.start() + + mock.call_start1.assert_called_once() + async_mock.call_start2.assert_called_once() + + +@pytest.mark.asyncio() +async def test_startup_calls_lifespans( + mock: MagicMock, + app: FastStream, + async_mock: AsyncMock, +) -> None: + def call1() -> None: + mock.call_start1() + assert not mock.call_start2.called + + def call2() -> None: + mock.call_start2() + assert mock.call_start1.call_count == 1 + + app.on_startup(call1) + app.on_startup(call2) + + with patch.object(app.broker, "start", async_mock): + await app.start() + + mock.call_start1.assert_called_once() + mock.call_start2.assert_called_once() + + +@pytest.mark.asyncio() +async def test_on_shutdown_calls(async_mock: AsyncMock, mock: MagicMock) -> None: + def call1() -> None: + mock.call_stop1() + assert not async_mock.call_stop2.called + + async def call2() -> None: + await async_mock.call_stop2() + assert mock.call_stop1.call_count == 1 + + test_app = FastStream(AsyncMock(), on_shutdown=[call1, call2]) + + await test_app.stop() + + mock.call_stop1.assert_called_once() + async_mock.call_stop2.assert_called_once() + + +@pytest.mark.asyncio() +async def test_shutdown_calls_lifespans(mock: MagicMock) -> None: + app = FastStream(AsyncMock()) + + def call1() -> None: + mock.call_stop1() + assert not mock.call_stop2.called + + def call2() -> None: + mock.call_stop2() + assert mock.call_stop1.call_count == 1 + + app.on_shutdown(call1) + app.on_shutdown(call2) + + await app.stop() + + mock.call_stop1.assert_called_once() + mock.call_stop2.assert_called_once() + + +@pytest.mark.asyncio() +async def test_after_startup_calls( + async_mock: AsyncMock, + mock: MagicMock, + broker: RabbitBroker, +) -> None: + def call1() -> None: + mock.after_startup1() + assert not async_mock.after_startup2.called + + async def call2() -> None: + await async_mock.after_startup2() + assert mock.after_startup1.call_count == 1 + + test_app = FastStream(broker, after_startup=[call1, call2]) + + with patch.object(test_app.broker, "start", async_mock.broker_start): + await test_app.start() + + mock.after_startup1.assert_called_once() + async_mock.after_startup2.assert_called_once() + + +@pytest.mark.asyncio() +async def test_startup_lifespan_before_broker_started( + async_mock: AsyncMock, + app: FastStream, +) -> None: + @app.on_startup + async def call() -> None: + await async_mock.before() + assert not async_mock.broker_start.called + + @app.after_startup + async def call_after() -> None: + await async_mock.after() + async_mock.before.assert_awaited_once() + async_mock.broker_start.assert_called_once() + + with ( + patch.object(app.broker, "start", async_mock.broker_start), + patch.object( + app.broker, + "connect", + async_mock.broker_connect, + ), + ): + await app.start() + + async_mock.broker_start.assert_called_once() + async_mock.after.assert_awaited_once() + async_mock.before.assert_awaited_once() + + +@pytest.mark.asyncio() +async def test_after_shutdown_calls( + async_mock: AsyncMock, + mock: MagicMock, + broker: RabbitBroker, +) -> None: + def call1() -> None: + mock.after_shutdown1() + assert not async_mock.after_shutdown2.called + + async def call2() -> None: + await async_mock.after_shutdown2() + assert mock.after_shutdown1.call_count == 1 + + test_app = FastStream(broker, after_shutdown=[call1, call2]) + + with ( + patch.object(test_app.broker, "start", async_mock.broker_start), + patch.object( + test_app.broker, + "connect", + async_mock.broker_connect, + ), + ): + await test_app.stop() + + mock.after_shutdown1.assert_called_once() + async_mock.after_shutdown2.assert_called_once() + + +@pytest.mark.asyncio() +async def test_shutdown_lifespan_after_broker_stopped( + async_mock: AsyncMock, + app: FastStream, +) -> None: + @app.after_shutdown + async def call() -> None: + await async_mock.after() + async_mock.broker_stop.assert_called_once() + + @app.on_shutdown + async def call_before() -> None: + await async_mock.before() + assert not async_mock.broker_stop.called + + with patch.object(app.broker, "close", async_mock.broker_stop): + await app.stop() + + async_mock.broker_stop.assert_called_once() + async_mock.after.assert_awaited_once() + async_mock.before.assert_awaited_once() + + +@pytest.mark.asyncio() +async def test_running(async_mock: AsyncMock, app: FastStream) -> None: + app.exit() + + with ( + patch.object(app.broker, "start", async_mock.broker_run), + patch.object(app.broker, "close", async_mock.broker_stopped), + ): + await app.run() + + async_mock.broker_run.assert_called_once() + async_mock.broker_stopped.assert_called_once() + + +@pytest.mark.asyncio() +async def test_exception_group(async_mock: AsyncMock, app: FastStream) -> None: + async_mock.side_effect = ValueError("Ooops!") + + @app.on_startup + async def f() -> None: + await async_mock() + + with pytest.raises(ValueError, match="Ooops!"): + await app.run() + + +@pytest.mark.asyncio() +async def test_running_lifespan_contextmanager( + async_mock: AsyncMock, + mock: MagicMock, + app: FastStream, +) -> None: + @asynccontextmanager + async def lifespan(env: str) -> AsyncIterator[None]: + mock.on(env) + yield + mock.off() + + app = FastStream(async_mock, lifespan=lifespan) + app.exit() + + await app.run(run_extra_options={"env": "test"}) + + async_mock.start.assert_called_once() + async_mock.close.assert_called_once() + + mock.on.assert_called_once_with("test") + mock.off.assert_called_once() + + +@pytest.mark.asyncio() +async def test_test_app(mock: MagicMock) -> None: + app = FastStream(AsyncMock()) + + app.on_startup(mock.on) + app.on_shutdown(mock.off) + + async with TestApp(app): + pass + + mock.on.assert_called_once() + mock.off.assert_called_once() + + +@pytest.mark.asyncio() +async def test_test_app_with_excp(mock: MagicMock) -> None: + app = FastStream(AsyncMock()) + + app.on_startup(mock.on) + app.on_shutdown(mock.off) + + with pytest.raises(ValueError): # noqa: PT011 + async with TestApp(app): + raise ValueError + + mock.on.assert_called_once() + mock.off.assert_called_once() + + +def test_sync_test_app(mock: MagicMock) -> None: + app = FastStream(AsyncMock()) + + app.on_startup(mock.on) + app.on_shutdown(mock.off) + + with TestApp(app): + pass + + mock.on.assert_called_once() + mock.off.assert_called_once() + + +def test_sync_test_app_with_excp(mock: MagicMock) -> None: + app = FastStream(AsyncMock()) + + app.on_startup(mock.on) + app.on_shutdown(mock.off) + + with pytest.raises(ValueError), TestApp(app): # noqa: PT011 + raise ValueError + + mock.on.assert_called_once() + mock.off.assert_called_once() + + +@pytest.mark.asyncio() +async def test_lifespan_contextmanager(async_mock: AsyncMock, app: FastStream) -> None: + @asynccontextmanager + async def lifespan(env: str) -> AsyncIterator[None]: + await async_mock.on(env) + yield + await async_mock.off() + + app = FastStream(app.broker, lifespan=lifespan) + + with ( + patch.object(app.broker, "start", async_mock.broker_run), + patch.object(app.broker, "close", async_mock.broker_stopped), + ): + async with TestApp(app, {"env": "test"}): + pass + + async_mock.on.assert_awaited_once_with("test") + async_mock.off.assert_awaited_once() + async_mock.broker_run.assert_called_once() + async_mock.broker_stopped.assert_called_once() + + +def test_sync_lifespan_contextmanager(async_mock: AsyncMock, app: FastStream) -> None: + @asynccontextmanager + async def lifespan(env: str) -> AsyncIterator[None]: + await async_mock.on(env) + yield + await async_mock.off() + + app = FastStream(app.broker, lifespan=lifespan) + + with ( + patch.object(app.broker, "start", async_mock.broker_run), + patch.object( + app.broker, + "close", + async_mock.broker_stopped, + ), + TestApp( + app, + {"env": "test"}, + ), + ): + pass + + async_mock.on.assert_awaited_once_with("test") + async_mock.off.assert_awaited_once() + async_mock.broker_run.assert_called_once() + async_mock.broker_stopped.assert_called_once() + + +@pytest.mark.asyncio() +@skip_windows +async def test_stop_with_sigint(async_mock: AsyncMock, app: FastStream) -> None: + with ( + patch.object(app.broker, "start", async_mock.broker_run_sigint), + patch.object(app.broker, "close", async_mock.broker_stopped_sigint), + ): + async with anyio.create_task_group() as tg: + tg.start_soon(app.run) + tg.start_soon(_kill, signal.SIGINT) + + async_mock.broker_run_sigint.assert_called_once() + async_mock.broker_stopped_sigint.assert_called_once() + + +@pytest.mark.asyncio() +@skip_windows +async def test_stop_with_sigterm(async_mock: AsyncMock, app: FastStream) -> None: + with ( + patch.object(app.broker, "start", async_mock.broker_run_sigterm), + patch.object(app.broker, "close", async_mock.broker_stopped_sigterm), + ): + async with anyio.create_task_group() as tg: + tg.start_soon(app.run) + tg.start_soon(_kill, signal.SIGTERM) + + async_mock.broker_run_sigterm.assert_called_once() + async_mock.broker_stopped_sigterm.assert_called_once() + + +@pytest.mark.asyncio() +@skip_windows +async def test_run_asgi(async_mock: AsyncMock, app: FastStream) -> None: + asgi_routes = [("/", AsgiResponse())] + asgi_app = app.as_asgi(asgi_routes=asgi_routes) + assert asgi_app.broker is app.broker + assert asgi_app.logger is app.logger + assert asgi_app.lifespan_context is app.lifespan_context + assert asgi_app._on_startup_calling is app._on_startup_calling + assert asgi_app._after_startup_calling is app._after_startup_calling + assert asgi_app._on_shutdown_calling is app._on_shutdown_calling + assert asgi_app._after_shutdown_calling is app._after_shutdown_calling + assert asgi_app.routes == asgi_routes + + with ( + patch.object(app.broker, "start", async_mock.broker_run), + patch.object(app.broker, "close", async_mock.broker_stopped), + ): + async with anyio.create_task_group() as tg: + tg.start_soon(app.run) + tg.start_soon(_kill, signal.SIGINT) + + async_mock.broker_run.assert_called_once() + async_mock.broker_stopped.assert_called_once() + + +async def _kill(sig: int) -> None: + os.kill(os.getpid(), sig) diff --git a/tests/cli/require_broker/test_logs.py b/tests/cli/require_broker/test_logs.py new file mode 100644 index 0000000000..750025f970 --- /dev/null +++ b/tests/cli/require_broker/test_logs.py @@ -0,0 +1,44 @@ +import logging + +import pytest + +from faststream import FastStream +from faststream._internal.cli.utils.logs import get_log_level, set_log_level +from faststream.rabbit import RabbitBroker + + +def test_set_level() -> None: + broker = RabbitBroker() + app = FastStream(broker) + set_log_level(logging.ERROR, app) + broker._setup_logger() + broker_logger = broker.config.logger.logger.logger + assert app.logger.level == broker_logger.level == logging.ERROR + + +def test_set_default(broker) -> None: + app = FastStream(broker) + level = "wrong_level" + set_log_level(get_log_level(level), app) + assert app.logger.level is logging.INFO + + +@pytest.mark.parametrize( + ("app"), + ( + pytest.param( + FastStream(RabbitBroker(), logger=None), + id="app without logger", + ), + pytest.param( + FastStream(RabbitBroker(logger=None)), + id="broker without logger", + ), + pytest.param( + FastStream(RabbitBroker(logger=None), logger=None), + id="both without logger", + ), + ), +) +def test_set_level_to_none(app: FastStream) -> None: + set_log_level(logging.CRITICAL, app) diff --git a/tests/cli/supervisors/test_base_reloader.py b/tests/cli/supervisors/test_base_reloader.py index ebc29e1d4e..f92309ffb6 100644 --- a/tests/cli/supervisors/test_base_reloader.py +++ b/tests/cli/supervisors/test_base_reloader.py @@ -1,8 +1,10 @@ import signal +from multiprocessing.context import SpawnProcess +from typing import Any import pytest -from faststream.cli.supervisors.basereload import BaseReload +from faststream._internal.cli.supervisors.basereload import BaseReload, get_subprocess class PatchedBaseReload(BaseReload): @@ -13,17 +15,22 @@ def restart(self) -> None: def should_restart(self) -> bool: return True + def start_process(self, worker_id: int | None = None) -> SpawnProcess: + process = get_subprocess(target=self._target, args=self._args) + process.start() + return process -def empty(*args, **kwargs): + +def empty(*args: Any, **kwargs: Any) -> None: pass -@pytest.mark.slow -def test_base(): +@pytest.mark.slow() +def test_base() -> None: processor = PatchedBaseReload(target=empty, args=()) processor._args = (processor.pid,) processor.run() code = abs(processor._process.exitcode or 0) - assert code == signal.SIGTERM.value or code == 0 + assert code in {signal.SIGTERM.value, 0} diff --git a/tests/cli/supervisors/test_multiprocess.py b/tests/cli/supervisors/test_multiprocess.py index d50ce7995d..73ec991f6a 100644 --- a/tests/cli/supervisors/test_multiprocess.py +++ b/tests/cli/supervisors/test_multiprocess.py @@ -1,23 +1,25 @@ import os import signal -import sys import pytest -from faststream.cli.supervisors.multiprocess import Multiprocess +from faststream._internal.cli.supervisors.multiprocess import Multiprocess +from tests.marks import skip_windows -def exit(parent_id): # pragma: no cover +def exit(parent_id: int, *args) -> None: # pragma: no cover os.kill(parent_id, signal.SIGINT) + raise SyntaxError -@pytest.mark.slow -@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") -def test_base(): - processor = Multiprocess(target=exit, args=(), workers=5) - processor._args = (processor.pid,) +@skip_windows +@pytest.mark.flaky(retries=3, retry_delay=1) +def test_base() -> None: + processor = Multiprocess(target=exit, args=(), workers=2) + processor._args = (processor.pid, {}) processor.run() for p in processor.processes: + assert p.exitcode code = abs(p.exitcode) - assert code == signal.SIGTERM.value or code == 0 + assert code in {signal.SIGTERM.value, 0} diff --git a/tests/cli/supervisors/test_watchfiles.py b/tests/cli/supervisors/test_watchfiles.py index 511ccac0a2..ad28f6a5e1 100644 --- a/tests/cli/supervisors/test_watchfiles.py +++ b/tests/cli/supervisors/test_watchfiles.py @@ -1,51 +1,64 @@ import os import signal -import sys import time +from multiprocessing.context import SpawnProcess from pathlib import Path -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, patch import pytest -from faststream.cli.supervisors.watchfiles import WatchReloader +from faststream._internal.cli.supervisors.utils import get_subprocess +from faststream._internal.cli.supervisors.watchfiles import WatchReloader +from tests.cli.conftest import GenerateTemplateFactory +from tests.marks import skip_windows DIR = Path(__file__).resolve().parent -def exit(parent_id): # pragma: no cover - os.kill(parent_id, signal.SIGINT) +class PatchedWatchReloader(WatchReloader): + def start_process(self, worker_id: int | None = None) -> SpawnProcess: + process = get_subprocess(target=self._target, args=self._args) + process.start() + return process -@pytest.mark.slow -@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") -def test_base(): - processor = WatchReloader(target=exit, args=(), reload_dirs=[DIR]) +@pytest.mark.slow() +@skip_windows +def test_base(generate_template: GenerateTemplateFactory) -> None: + with generate_template("") as file_path: + processor = PatchedWatchReloader( + target=exit, args=(), reload_dirs=[str(file_path.parent)] + ) - processor._args = (processor.pid,) - processor.run() + processor._args = (processor.pid,) + processor.run() - code = abs(processor._process.exitcode) - assert code == signal.SIGTERM.value or code == 0 + code = abs(processor._process.exitcode or 0) + assert code in {signal.SIGTERM.value, 0}, code -def touch_file(file: Path): # pragma: no cover - while True: - time.sleep(0.1) - with file.open("a") as f: - f.write("hello") +@pytest.mark.slow() +@skip_windows +def test_restart(mock: MagicMock, generate_template: GenerateTemplateFactory) -> None: + with generate_template("") as file_path: + processor = PatchedWatchReloader( + target=touch_file, args=(file_path,), reload_dirs=[file_path.parent] + ) -@pytest.mark.slow -@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") -def test_restart(mock: Mock): - file = DIR / "file.py" + mock.side_effect = lambda: exit(processor.pid) - processor = WatchReloader(target=touch_file, args=(file,), reload_dirs=[DIR]) + with patch.object(processor, "restart", mock): + processor.run() - mock.side_effect = lambda: os.kill(processor.pid, signal.SIGINT) + mock.assert_called_once() - with patch.object(processor, "restart", mock): - processor.run() - mock.assert_called_once() - file.unlink() +def touch_file(file: Path) -> None: + while True: + time.sleep(0.1) + file.write_text("hello") + + +def exit(parent_id: int) -> None: + os.kill(parent_id, signal.SIGINT) diff --git a/tests/cli/test_asyncapi_docs.py b/tests/cli/test_asyncapi_docs.py index 4ec0b75b33..19940d985b 100644 --- a/tests/cli/test_asyncapi_docs.py +++ b/tests/cli/test_asyncapi_docs.py @@ -1,13 +1,13 @@ import json -import urllib.request -from typing import Any, Callable, List, TextIO +from collections.abc import Callable +from typing import Any, TextIO +import httpx import pytest import yaml -from faststream._compat import IS_WINDOWS from tests.cli.conftest import FastStreamCLIFactory, GenerateTemplateFactory -from tests.marks import require_aiokafka +from tests.marks import require_aiokafka, skip_windows json_asyncapi_doc = """ { @@ -120,6 +120,7 @@ from pydantic import BaseModel, Field, NonNegativeFloat from faststream import FastStream, Logger +from faststream.specification import AsyncAPI from faststream.kafka import KafkaBroker @@ -130,6 +131,7 @@ class DataBasic(BaseModel): broker = KafkaBroker("localhost:9092") +doc = AsyncAPI(broker) app = FastStream(broker) @@ -138,97 +140,87 @@ class DataBasic(BaseModel): async def on_input_data(msg: DataBasic, logger: Logger) -> DataBasic: logger.info(msg) return DataBasic(data=msg.data + 1.0) - """ -@pytest.mark.slow +@pytest.mark.slow() @require_aiokafka @pytest.mark.parametrize( - ("doc_flag", "load_schema"), - [ - pytest.param([], lambda f: json.load(f), id="json"), + ("commands", "load_schema"), + ( + pytest.param( + [], + json.load, + id="json", + ), pytest.param( - ["--yaml"], lambda f: yaml.load(f, Loader=yaml.BaseLoader), id="yaml" + ["--yaml"], + lambda f: yaml.load(f, Loader=yaml.BaseLoader), + id="yaml", ), - ], + ), ) def test_gen_asyncapi_for_kafka_app( + commands: list[str], generate_template: GenerateTemplateFactory, faststream_cli: FastStreamCLIFactory, - doc_flag: List[str], load_schema: Callable[[TextIO], Any], ) -> None: - with generate_template(app_code) as app_path, faststream_cli( - [ + with ( + generate_template(app_code) as app_path, + faststream_cli( "faststream", "docs", "gen", - *doc_flag, - f"{app_path.stem}:app", + f"{app_path.stem}:doc", "--out", str(app_path.parent / "schema.json"), - ], - ) as cli_thread: + *commands, + ) as cli_thread, + ): assert cli_thread.process - assert cli_thread.process.returncode == 0 - - schema_path = app_path.parent / "schema.json" - assert schema_path.exists() + schema_path = app_path.parent / "schema.json" + assert schema_path.exists() - with schema_path.open() as f: - schema = load_schema(f) + with schema_path.open() as f: + schema = load_schema(f) - assert schema - schema_path.unlink() + assert schema + schema_path.unlink() -@pytest.mark.slow +@pytest.mark.slow() def test_gen_wrong_path(faststream_cli: FastStreamCLIFactory) -> None: - with faststream_cli( - [ - "faststream", - "docs", - "gen", - "non_existent:app", - ], - ) as cli_thread: - assert cli_thread.process - assert cli_thread.process - assert cli_thread.process.returncode == 2 - assert cli_thread.process.stderr - assert "No such file or directory" in cli_thread.process.stderr.read() + with faststream_cli("faststream", "docs", "gen", "non_existent:doc") as cli: + assert cli.wait_for_stderr("No such file or directory") -@pytest.mark.slow -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") +@pytest.mark.slow() +@skip_windows @require_aiokafka def test_serve_asyncapi_docs_from_app( generate_template: GenerateTemplateFactory, faststream_cli: FastStreamCLIFactory, ) -> None: - with generate_template(app_code) as app_path, faststream_cli( - [ - "faststream", - "docs", - "serve", - f"{app_path.stem}:app", - ], - ), urllib.request.urlopen("http://localhost:8000") as response: - assert "FastStream AsyncAPI" in response.read().decode() - assert response.getcode() == 200 + with ( + generate_template(app_code) as app_path, + faststream_cli("faststream", "docs", "serve", f"{app_path.stem}:doc"), + ): + response = httpx.get("http://localhost:8000") + assert "FastStream AsyncAPI" in response.text + assert response.status_code == 200 -@pytest.mark.slow -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") +@pytest.mark.slow() +@skip_windows @require_aiokafka @pytest.mark.parametrize( ("doc_filename", "doc"), - [ + ( pytest.param("asyncapi.json", json_asyncapi_doc, id="json_schema"), pytest.param("asyncapi.yaml", yaml_asyncapi_doc, id="yaml_schema"), - ], + ), ) def test_serve_asyncapi_docs_from_file( doc_filename: str, @@ -236,13 +228,10 @@ def test_serve_asyncapi_docs_from_file( generate_template: GenerateTemplateFactory, faststream_cli: FastStreamCLIFactory, ) -> None: - with generate_template(doc, filename=doc_filename) as doc_path, faststream_cli( - [ - "faststream", - "docs", - "serve", - str(doc_path), - ], - ), urllib.request.urlopen("http://localhost:8000") as response: - assert "FastStream AsyncAPI" in response.read().decode() - assert response.getcode() == 200 + with ( + generate_template(doc, filename=doc_filename) as doc_path, + faststream_cli("faststream", "docs", "serve", str(doc_path)), + ): + response = httpx.get("http://localhost:8000") + assert "FastStream AsyncAPI" in response.text + assert response.status_code == 200 diff --git a/tests/cli/test_logs.py b/tests/cli/test_logs.py new file mode 100644 index 0000000000..7dbf765a07 --- /dev/null +++ b/tests/cli/test_logs.py @@ -0,0 +1,183 @@ +import logging +import random +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from typer.testing import CliRunner + +from faststream import FastStream +from faststream._internal.cli.main import cli as faststream_app +from faststream._internal.cli.utils.logs import get_log_level +from tests.marks import skip_windows + +from .conftest import FastStreamCLIFactory, GenerateTemplateFactory + + +@pytest.mark.parametrize( + ( + "level", + "expected_level", + ), + ( + pytest.param("critical", logging.CRITICAL), + pytest.param("fatal", logging.FATAL), + pytest.param("error", logging.ERROR), + pytest.param("warning", logging.WARNING), + pytest.param("warn", logging.WARNING), + pytest.param("info", logging.INFO), + pytest.param("debug", logging.DEBUG), + pytest.param("notset", logging.NOTSET), + ), +) +def test_get_level(level: str, expected_level: int) -> None: + assert get_log_level(level) == expected_level + + +@pytest.mark.slow() +@skip_windows +@pytest.mark.parametrize( + ("log_config_file_name", "log_config"), + ( + pytest.param( + "config.json", + """ + { + "version": 1, + "loggers": { + "unique_logger_name": { + "level": 42 + } + } + } + """, + id="json config", + ), + pytest.param( + "config.toml", + """ + version = 1 + + [loggers.unique_logger_name] + level = 42 + """, + id="toml config", + ), + pytest.param( + "config.yaml", + """ + version: 1 + loggers: + unique_logger_name: + level: 42 + """, + id="yaml config", + ), + ), +) +def test_run_as_asgi_with_log_config( + generate_template: GenerateTemplateFactory, + faststream_cli: FastStreamCLIFactory, + log_config_file_name: str, + log_config: str, +) -> None: + app_code = """ + import logging + + from faststream.asgi import AsgiFastStream + from faststream.nats import NatsBroker + + broker = NatsBroker() + + app = AsgiFastStream(broker) + + logger = logging.getLogger("faststream") + + @app.on_startup + def print_log_level() -> None: + logger.critical(f"Current log level is {logging.getLogger('unique_logger_name').level}") + """ + with ( + generate_template(app_code) as app_path, + generate_template( + log_config, filename=log_config_file_name + ) as log_config_file_path, + faststream_cli( + "faststream", + "run", + f"{app_path.stem}:app", + "--log-config", + str(log_config_file_path), + ) as cli, + ): + assert cli.wait_for_stderr("Current log level is 42") + + +@pytest.mark.slow() +@skip_windows +def test_run_as_asgi_mp_with_log_level( + generate_template: GenerateTemplateFactory, + faststream_cli: FastStreamCLIFactory, +) -> None: + app_code = """ + import logging + + from faststream.asgi import AsgiFastStream + from faststream.nats import NatsBroker + + app = AsgiFastStream(NatsBroker()) + + logger = logging.getLogger("faststream") + + @app.on_startup + def print_log_level() -> None: + logger.critical(f"Current log level is {logging.getLogger('uvicorn.asgi').level}") + """ + log_level, numeric_log_level = "warn", 30 + + with ( + generate_template(app_code) as app_path, + faststream_cli( + "faststream", + "run", + f"{app_path.stem}:app", + "--workers", + str(random.randint(2, 7)), + "--log-level", + log_level, + ) as cli, + ): + assert cli.wait_for_stderr(f"Current log level is {numeric_log_level}") + + +def test_run_with_log_level(runner: CliRunner) -> None: + app = FastStream(MagicMock()) + app.run = AsyncMock() + + with patch( + "faststream._internal.cli.utils.imports._import_object_or_factory", + return_value=(None, app), + ): + result = runner.invoke( + faststream_app, + ["run", "-l", "warning", "faststream:app"], + ) + + assert result.exit_code == 0, result.output + + assert app.logger.level == logging.WARNING + + +def test_run_with_wrong_log_level(runner: CliRunner) -> None: + app = FastStream(MagicMock()) + app.run = AsyncMock() + + with patch( + "faststream._internal.cli.utils.imports._import_object_or_factory", + return_value=(None, app), + ): + result = runner.invoke( + faststream_app, + ["run", "-l", "30", "faststream:app"], + ) + + assert result.exit_code == 2, result.output diff --git a/tests/cli/test_publish.py b/tests/cli/test_publish.py index 0887e95c8c..1034bab40e 100644 --- a/tests/cli/test_publish.py +++ b/tests/cli/test_publish.py @@ -1,9 +1,11 @@ +from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, patch -from dirty_equals import IsPartialDict +from typer.testing import CliRunner from faststream import FastStream -from faststream.cli.main import cli as faststream_app +from faststream._internal.cli.main import cli as faststream_app +from faststream.response.publish_type import PublishType from tests.marks import ( require_aiokafka, require_aiopika, @@ -12,25 +14,36 @@ require_redis, ) +if TYPE_CHECKING: + from faststream.confluent.response import ( + KafkaPublishCommand as ConfluentPublishCommand, + ) + from faststream.kafka.response import KafkaPublishCommand + from faststream.nats.response import NatsPublishCommand + from faststream.rabbit.response import RabbitPublishCommand + from faststream.redis.response import RedisPublishCommand -def get_mock_app(broker_type, producer_type) -> FastStream: + +def get_mock_app(broker_type: Any, producer_type: Any) -> tuple[FastStream, AsyncMock]: broker = broker_type() broker.connect = AsyncMock() mock_producer = AsyncMock(spec=producer_type) mock_producer.publish = AsyncMock() - broker._producer = mock_producer - return FastStream(broker) + mock_producer._parser = AsyncMock() + mock_producer._decoder = AsyncMock() + broker.config.broker_config.producer = mock_producer + return FastStream(broker), mock_producer @require_redis -def test_publish_command_with_redis_options(runner): +def test_publish_command_with_redis_options(runner: CliRunner) -> None: from faststream.redis import RedisBroker from faststream.redis.publisher.producer import RedisFastProducer - mock_app = get_mock_app(RedisBroker, RedisFastProducer) + mock_app, producer_mock = get_mock_app(RedisBroker, RedisFastProducer) with patch( - "faststream.cli.utils.imports._import_obj_or_factory", + "faststream._internal.cli.utils.imports._import_object_or_factory", return_value=(None, mock_app), ): result = runner.invoke( @@ -40,13 +53,9 @@ def test_publish_command_with_redis_options(runner): "fastream:app", "hello world", "--channel", - "test channel", + "channelname", "--reply_to", "tester", - "--list", - "0.1", - "--stream", - "stream url", "--correlation_id", "someId", ], @@ -54,26 +63,22 @@ def test_publish_command_with_redis_options(runner): assert result.exit_code == 0 - assert mock_app.broker._producer.publish.call_args.args[0] == "hello world" - assert mock_app.broker._producer.publish.call_args.kwargs == IsPartialDict( - channel="test channel", - reply_to="tester", - list="0.1", - stream="stream url", - correlation_id="someId", - rpc=False, - ) + cmd: RedisPublishCommand = producer_mock.publish.call_args.args[0] + assert cmd.body == "hello world" + assert cmd.reply_to == "tester" + assert cmd.destination == "channelname" + assert cmd.correlation_id == "someId" @require_confluent -def test_publish_command_with_confluent_options(runner): +def test_publish_command_with_confluent_options(runner: CliRunner) -> None: from faststream.confluent import KafkaBroker as ConfluentBroker from faststream.confluent.publisher.producer import AsyncConfluentFastProducer - mock_app = get_mock_app(ConfluentBroker, AsyncConfluentFastProducer) + mock_app, producer_mock = get_mock_app(ConfluentBroker, AsyncConfluentFastProducer) with patch( - "faststream.cli.utils.imports._import_obj_or_factory", + "faststream._internal.cli.utils.imports._import_object_or_factory", return_value=(None, mock_app), ): result = runner.invoke( @@ -83,30 +88,29 @@ def test_publish_command_with_confluent_options(runner): "fastream:app", "hello world", "--topic", - "confluent topic", + "topicname", "--correlation_id", "someId", ], ) assert result.exit_code == 0 - assert mock_app.broker._producer.publish.call_args.args[0] == "hello world" - assert mock_app.broker._producer.publish.call_args.kwargs == IsPartialDict( - topic="confluent topic", - correlation_id="someId", - rpc=False, - ) + + cmd: ConfluentPublishCommand = producer_mock.publish.call_args.args[0] + assert cmd.body == "hello world" + assert cmd.destination == "topicname" + assert cmd.correlation_id == "someId" @require_aiokafka -def test_publish_command_with_kafka_options(runner): +def test_publish_command_with_kafka_options(runner: CliRunner) -> None: from faststream.kafka import KafkaBroker from faststream.kafka.publisher.producer import AioKafkaFastProducer - mock_app = get_mock_app(KafkaBroker, AioKafkaFastProducer) + mock_app, producer_mock = get_mock_app(KafkaBroker, AioKafkaFastProducer) with patch( - "faststream.cli.utils.imports._import_obj_or_factory", + "faststream._internal.cli.utils.imports._import_object_or_factory", return_value=(None, mock_app), ): result = runner.invoke( @@ -116,30 +120,29 @@ def test_publish_command_with_kafka_options(runner): "fastream:app", "hello world", "--topic", - "kafka topic", + "topicname", "--correlation_id", "someId", ], ) assert result.exit_code == 0 - assert mock_app.broker._producer.publish.call_args.args[0] == "hello world" - assert mock_app.broker._producer.publish.call_args.kwargs == IsPartialDict( - topic="kafka topic", - correlation_id="someId", - rpc=False, - ) + + cmd: KafkaPublishCommand = producer_mock.publish.call_args.args[0] + assert cmd.body == "hello world" + assert cmd.destination == "topicname" + assert cmd.correlation_id == "someId" @require_nats -def test_publish_command_with_nats_options(runner): +def test_publish_command_with_nats_options(runner: CliRunner) -> None: from faststream.nats import NatsBroker from faststream.nats.publisher.producer import NatsFastProducer - mock_app = get_mock_app(NatsBroker, NatsFastProducer) + mock_app, producer_mock = get_mock_app(NatsBroker, NatsFastProducer) with patch( - "faststream.cli.utils.imports._import_obj_or_factory", + "faststream._internal.cli.utils.imports._import_object_or_factory", return_value=(None, mock_app), ): result = runner.invoke( @@ -149,7 +152,7 @@ def test_publish_command_with_nats_options(runner): "fastream:app", "hello world", "--subject", - "nats subject", + "subjectname", "--reply_to", "tester", "--correlation_id", @@ -159,24 +162,22 @@ def test_publish_command_with_nats_options(runner): assert result.exit_code == 0 - assert mock_app.broker._producer.publish.call_args.args[0] == "hello world" - assert mock_app.broker._producer.publish.call_args.kwargs == IsPartialDict( - subject="nats subject", - reply_to="tester", - correlation_id="someId", - rpc=False, - ) + cmd: NatsPublishCommand = producer_mock.publish.call_args.args[0] + assert cmd.body == "hello world" + assert cmd.destination == "subjectname" + assert cmd.reply_to == "tester" + assert cmd.correlation_id == "someId" @require_aiopika -def test_publish_command_with_rabbit_options(runner): +def test_publish_command_with_rabbit_options(runner: CliRunner) -> None: from faststream.rabbit import RabbitBroker from faststream.rabbit.publisher.producer import AioPikaFastProducer - mock_app = get_mock_app(RabbitBroker, AioPikaFastProducer) + mock_app, producer_mock = get_mock_app(RabbitBroker, AioPikaFastProducer) with patch( - "faststream.cli.utils.imports._import_obj_or_factory", + "faststream._internal.cli.utils.imports._import_object_or_factory", return_value=(None, mock_app), ): result = runner.invoke( @@ -185,20 +186,48 @@ def test_publish_command_with_rabbit_options(runner): "publish", "fastream:app", "hello world", + "--queue", + "queuename", "--correlation_id", "someId", - "--raise_timeout", - "True", ], ) assert result.exit_code == 0 - assert mock_app.broker._producer.publish.call_args.args[0] == "hello world" - assert mock_app.broker._producer.publish.call_args.kwargs == IsPartialDict( - { - "correlation_id": "someId", - "raise_timeout": "True", - "rpc": False, - } + cmd: RabbitPublishCommand = producer_mock.publish.call_args.args[0] + assert cmd.body == "hello world" + assert cmd.destination == "queuename" + assert cmd.correlation_id == "someId" + + +@require_nats +def test_publish_nats_request_command(runner: CliRunner) -> None: + from faststream.nats import NatsBroker + from faststream.nats.publisher.producer import NatsFastProducer + + mock_app, producer_mock = get_mock_app(NatsBroker, NatsFastProducer) + + with patch( + "faststream._internal.cli.utils.imports._import_object_or_factory", + return_value=(None, mock_app), + ): + runner.invoke( + faststream_app, + [ + "publish", + "fastream:app", + "hello world", + "--subject", + "subjectname", + "--rpc", + "--timeout", + "1.0", + ], ) + + cmd: NatsPublishCommand = producer_mock.request.call_args.args[0] + + assert cmd.destination == "subjectname" + assert cmd.timeout == 1.0 + assert cmd.publish_type is PublishType.REQUEST diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 323cd5471e..081ca00c51 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -1,352 +1,29 @@ -import json -import random -import urllib.request - -import psutil import pytest -from faststream._compat import IS_WINDOWS -from tests.cli.conftest import FastStreamCLIFactory, GenerateTemplateFactory - - -@pytest.mark.slow -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") -def test_run( - generate_template: GenerateTemplateFactory, faststream_cli: FastStreamCLIFactory -) -> None: - app_code = """ - from faststream import FastStream - from faststream.nats import NatsBroker - - broker = NatsBroker() - - app = FastStream(broker) - """ - with generate_template(app_code) as app_path, faststream_cli( - [ - "faststream", - "run", - f"{app_path.stem}:app", - ], - ) as cli_thread: - pass - assert cli_thread.process - - assert cli_thread.process.returncode == 0 - - -@pytest.mark.slow -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") -def test_run_asgi( - generate_template: GenerateTemplateFactory, faststream_cli: FastStreamCLIFactory -) -> None: - app_code = """ - import json - - from faststream import FastStream - from faststream.nats import NatsBroker - from faststream.asgi import AsgiResponse, get - - broker = NatsBroker() - - @get - async def liveness_ping(scope): - return AsgiResponse(b"hello world", status_code=200) - - - CONTEXT = {} - - @get - async def context(scope): - return AsgiResponse(json.dumps(CONTEXT).encode(), status_code=200) - - - app = FastStream(broker).as_asgi( - asgi_routes=[ - ("/liveness", liveness_ping), - ("/context", context) - ], - asyncapi_path="/docs", - ) - - @app.on_startup - async def start(test: int, port: int): - CONTEXT["test"] = test - CONTEXT["port"] = port - - """ - with generate_template(app_code) as app_path: - port = random.randrange(40000, 65535) - extra_param = random.randrange(1, 100) - - with faststream_cli( - [ - "faststream", - "run", - f"{app_path.stem}:app", - "--port", - f"{port}", - "--test", - f"{extra_param}", - ], - ): - with urllib.request.urlopen( - f"http://127.0.0.1:{port}/liveness" - ) as response: - assert response.read().decode() == "hello world" - assert response.getcode() == 200 - - with urllib.request.urlopen(f"http://127.0.0.1:{port}/docs") as response: - content = response.read().decode() - assert content.strip().startswith("") - assert len(content) > 1200 - - with urllib.request.urlopen(f"http://127.0.0.1:{port}/context") as response: - data = json.loads(response.read().decode()) - assert data == {"test": extra_param, "port": port} - assert response.getcode() == 200 - - -@pytest.mark.slow -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") -def test_run_as_asgi_with_single_worker( - generate_template: GenerateTemplateFactory, faststream_cli: FastStreamCLIFactory -) -> None: - app_code = """ - from faststream.asgi import AsgiFastStream, AsgiResponse, get - from faststream.nats import NatsBroker - - broker = NatsBroker() - - @get - async def liveness_ping(scope): - return AsgiResponse(b"hello world", status_code=200) - - app = AsgiFastStream(broker, asgi_routes=[ - ("/liveness", liveness_ping), - ]) - """ - with generate_template(app_code) as app_path, faststream_cli( - [ - "faststream", - "run", - f"{app_path.stem}:app", - "--workers", - "1", - ], - ), urllib.request.urlopen("http://127.0.0.1:8000/liveness") as response: - assert response.read().decode() == "hello world" - assert response.getcode() == 200 - +from faststream._internal._compat import IS_WINDOWS -@pytest.mark.slow -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") -@pytest.mark.parametrize("workers", [3, 5, 7]) -def test_run_as_asgi_with_many_workers( - generate_template: GenerateTemplateFactory, - faststream_cli: FastStreamCLIFactory, - workers: int, -) -> None: - app_code = """ - from faststream.asgi import AsgiFastStream - from faststream.nats import NatsBroker - - broker = NatsBroker() - - app = AsgiFastStream(broker) - """ - - with generate_template(app_code) as app_path, faststream_cli( - [ - "faststream", - "run", - f"{app_path.stem}:app", - "--workers", - str(workers), - ], - ) as cli_thread: - assert cli_thread.process - process = psutil.Process(pid=cli_thread.process.pid) - - assert len(process.children()) == workers + 1 - - -@pytest.mark.slow -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") -@pytest.mark.parametrize( - ("log_level", "numeric_log_level"), - [ - ("critical", 50), - ("fatal", 50), - ("error", 40), - ("warning", 30), - ("warn", 30), - ("info", 20), - ("debug", 10), - ("notset", 0), - ], -) -def test_run_as_asgi_mp_with_log_level( - generate_template: GenerateTemplateFactory, - faststream_cli: FastStreamCLIFactory, - log_level: str, - numeric_log_level: int, -) -> None: - app_code = """ - import logging - - from faststream.asgi import AsgiFastStream - from faststream.log.logging import logger - from faststream.nats import NatsBroker - - broker = NatsBroker() - - app = AsgiFastStream(broker) - - @app.on_startup - def print_log_level(): - logger.critical(f"Current log level is {logging.getLogger('uvicorn.asgi').level}") - """ - - with generate_template(app_code) as app_path, faststream_cli( - [ - "faststream", - "run", - f"{app_path.stem}:app", - "--workers", - "3", - "--log-level", - log_level, - ], - ) as cli_thread: - pass - assert cli_thread.process - assert cli_thread.process.stderr - stderr = cli_thread.process.stderr.read() - - assert f"Current log level is {numeric_log_level}" in stderr +from .conftest import FastStreamCLIFactory, GenerateTemplateFactory -@pytest.mark.slow -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") -def test_run_as_factory( - generate_template: GenerateTemplateFactory, faststream_cli: FastStreamCLIFactory -) -> None: - app_code = """ - from faststream.asgi import AsgiFastStream, AsgiResponse, get - from faststream.nats import NatsBroker - - broker = NatsBroker() - - @get - async def liveness_ping(scope): - return AsgiResponse(b"hello world", status_code=200) - - def app_factory(): - return AsgiFastStream(broker, asgi_routes=[ - ("/liveness", liveness_ping), - ]) - """ - - with generate_template(app_code) as app_path, faststream_cli( - [ - "faststream", - "run", - f"{app_path.stem}:app_factory", - "--factory", - ], - ), urllib.request.urlopen("http://127.0.0.1:8000/liveness") as response: - assert response.read().decode() == "hello world" - assert response.getcode() == 200 - - -@pytest.mark.slow -@pytest.mark.skipif(IS_WINDOWS, reason="does not run on windows") -@pytest.mark.parametrize( - ("log_config_file_name", "log_config"), - [ - pytest.param( - "config.json", - """ - { - "version": 1, - "loggers": { - "unique_logger_name": { - "level": 42 - } - } - } - """, - id="json config", - ), - pytest.param( - "config.toml", - """ - version = 1 - - [loggers.unique_logger_name] - level = 42 - """, - id="toml config", - ), - pytest.param( - "config.yaml", - """ - version: 1 - loggers: - unique_logger_name: - level: 42 - """, - id="yaml config", - ), - pytest.param( - "config.yml", - """ - version: 1 - loggers: - unique_logger_name: - level: 42 - """, - id="yml config", - ), - ], -) -def test_run_as_asgi_with_log_config( +@pytest.mark.slow() +def test_run( generate_template: GenerateTemplateFactory, faststream_cli: FastStreamCLIFactory, - log_config_file_name: str, - log_config: str, ) -> None: app_code = """ - import logging - - from faststream.asgi import AsgiFastStream - from faststream.log.logging import logger + from faststream import FastStream from faststream.nats import NatsBroker - broker = NatsBroker() - - app = AsgiFastStream(broker) - - @app.on_startup - def print_log_level(): - logger.critical(f"Current log level is {logging.getLogger('unique_logger_name').level}") + app = FastStream(NatsBroker()) """ - - with generate_template(app_code) as app_path, generate_template( - log_config, filename=log_config_file_name - ) as log_config_file_path, faststream_cli( - [ - "faststream", - "run", - f"{app_path.stem}:app", - "--log-config", - str(log_config_file_path), - ], - ) as cli_thread: - pass - assert cli_thread.process - assert cli_thread.process.stderr - stderr = cli_thread.process.stderr.read() - - assert "Current log level is 42" in stderr + with ( + generate_template(app_code) as app_path, + faststream_cli("faststream", "run", f"{app_path.stem}:app") as cli, + ): + cli.signint() + cli.wait(3.0) + + if IS_WINDOWS: + assert cli.process.returncode == 1 + else: + assert cli.process.returncode == 0 diff --git a/tests/cli/test_run_asgi.py b/tests/cli/test_run_asgi.py new file mode 100644 index 0000000000..e3c0c35655 --- /dev/null +++ b/tests/cli/test_run_asgi.py @@ -0,0 +1,159 @@ +import random + +import httpx +import psutil +import pytest + +from tests.marks import skip_windows + +from .conftest import FastStreamCLIFactory, GenerateTemplateFactory + + +@pytest.mark.slow() +@skip_windows +def test_run( + generate_template: GenerateTemplateFactory, + faststream_cli: FastStreamCLIFactory, +) -> None: + app_code = """ + import json + + from faststream import FastStream, specification + from faststream.rabbit import RabbitBroker + from faststream.asgi import AsgiResponse, get, make_asyncapi_asgi + + CONTEXT = {} + + @get + async def context(scope): + return AsgiResponse(json.dumps(CONTEXT).encode(), status_code=200) + + broker = RabbitBroker() + app = FastStream(broker).as_asgi( + asgi_routes=[ + ("/context", context), + ("/liveness", AsgiResponse(b"hello world", status_code=200)), + ("/docs", make_asyncapi_asgi(specification.AsyncAPI(broker))), + ], + ) + + @app.on_startup + async def start(test: int, port: int) -> None: + CONTEXT["test"] = test + CONTEXT["port"] = port + """ + port = random.randrange(40000, 65535) + extra_param = random.randrange(1, 100) + + with ( + generate_template(app_code) as app_path, + faststream_cli( + "faststream", + "run", + f"{app_path.stem}:app", + "--port", + f"{port}", + "--test", + f"{extra_param}", + ), + ): + # Test liveness + response = httpx.get(f"http://127.0.0.1:{port}/liveness") + assert response.text == "hello world" + assert response.status_code == 200 + + # Test documentation + response = httpx.get(f"http://127.0.0.1:{port}/docs") + assert response.text.strip().startswith("") + assert len(response.text) > 1200 + + # Test extra context + response = httpx.get(f"http://127.0.0.1:{port}/context") + assert response.json() == {"test": extra_param, "port": port} + assert response.status_code == 200 + + +@pytest.mark.slow() +@skip_windows +def test_single_worker( + generate_template: GenerateTemplateFactory, faststream_cli: FastStreamCLIFactory +) -> None: + app_code = """ + from faststream.asgi import AsgiFastStream, AsgiResponse + from faststream.nats import NatsBroker + + broker = NatsBroker() + + app = AsgiFastStream(broker, asgi_routes=[ + ("/liveness", AsgiResponse(b"hello world", status_code=200)), + ]) + """ + with ( + generate_template(app_code) as app_path, + faststream_cli( + "faststream", + "run", + f"{app_path.stem}:app", + "--workers", + "1", + ), + ): + response = httpx.get("http://127.0.0.1:8000/liveness") + assert response.text == "hello world" + assert response.status_code == 200 + + +@pytest.mark.slow() +@skip_windows +@pytest.mark.flaky(retries=3, retry_delay=1) +def test_many_workers( + generate_template: GenerateTemplateFactory, faststream_cli: FastStreamCLIFactory +) -> None: + app_code = """ + from faststream.asgi import AsgiFastStream + from faststream.nats import NatsBroker + + app = AsgiFastStream(NatsBroker()) + """ + + workers = 2 + + with ( + generate_template(app_code) as app_path, + faststream_cli( + "faststream", "run", f"{app_path.stem}:app", "--workers", str(workers) + ) as cli_thread, + ): + process = psutil.Process(pid=cli_thread.process.pid) + assert len(process.children()) == workers + 1 # 1 for the main process + + +@pytest.mark.slow() +@skip_windows +def test_factory( + generate_template: GenerateTemplateFactory, faststream_cli: FastStreamCLIFactory +) -> None: + app_code = """ + from faststream.asgi import AsgiFastStream, AsgiResponse, get + from faststream.nats import NatsBroker + + broker = NatsBroker() + + def app_factory(): + return AsgiFastStream(broker, asgi_routes=[ + ("/liveness", AsgiResponse(b"hello world", status_code=200)), + ]) + """ + + with ( + generate_template(app_code) as app_path, + faststream_cli( + "faststream", + "run", + f"{app_path.stem}:app_factory", + "--factory", + ), + ): + response = httpx.get("http://127.0.0.1:8000/liveness") + assert response.text == "hello world" + assert response.status_code == 200 diff --git a/tests/cli/test_version.py b/tests/cli/test_version.py index 98fbfd5c23..d97323827a 100644 --- a/tests/cli/test_version.py +++ b/tests/cli/test_version.py @@ -1,11 +1,15 @@ import platform -from faststream.cli.main import cli +from typer.testing import CliRunner +from faststream._internal.cli.main import cli -def test_version(runner, version): + +def test_version(runner: CliRunner, version: str) -> None: result = runner.invoke(cli, ["--version"]) + assert result.exit_code == 0 + assert version in result.stdout assert platform.python_implementation() in result.stdout assert platform.python_version() in result.stdout diff --git a/tests/cli/test_worker_id_extra_option.py b/tests/cli/test_worker_id_extra_option.py new file mode 100644 index 0000000000..8c18155cdf --- /dev/null +++ b/tests/cli/test_worker_id_extra_option.py @@ -0,0 +1,69 @@ +import pytest + +from tests.cli.conftest import FastStreamCLIFactory, GenerateTemplateFactory +from tests.marks import skip_windows + + +@pytest.mark.slow() +@skip_windows +@pytest.mark.parametrize( + ("app_import"), + ( + pytest.param( + "from faststream import FastStream", + id="default_app", + ), + pytest.param( + "from faststream.asgi import AsgiFastStream", + id="asgi_app", + ), + ), +) +@pytest.mark.parametrize( + ("log_strings", "cli_options"), + ( + pytest.param( + ["Worker id is None"], + [], + id="single_worker", + ), + pytest.param( + ["Worker id is 0", "Worker id is 1"], + ["--workers", "2"], + id="many_workers", + ), + ), +) +def test_worker_id_parameter_exists( + generate_template: GenerateTemplateFactory, + faststream_cli: FastStreamCLIFactory, + app_import: str, + log_strings: list[str], + cli_options: list[str], +) -> None: + app_code = f""" + import logging + + {app_import} as FastStreamApp + from faststream.nats import NatsBroker + + logger = logging.getLogger("faststream") + + broker = NatsBroker() + app = FastStreamApp(broker) + + @app.on_startup + def print_log_level(worker_id): + logger.critical("Worker id is %s", worker_id) + """ + + with ( + generate_template(app_code) as app_path, + faststream_cli( + "faststream", + "run", + f"{app_path.stem}:app", + *cli_options, + ) as cli, + ): + assert all(cli.wait_for_stderr(log_string, timeout=4) for log_string in log_strings) diff --git a/tests/cli/utils/test_imports.py b/tests/cli/utils/test_imports.py index f97e26c0ff..11263bc3c7 100644 --- a/tests/cli/utils/test_imports.py +++ b/tests/cli/utils/test_imports.py @@ -3,20 +3,24 @@ import pytest from typer import BadParameter +from faststream._internal.cli.utils.imports import ( + _get_obj_path, + _import_object, + import_from_string, +) from faststream.app import FastStream -from faststream.cli.utils.imports import get_app_path, import_from_string, import_object from tests.marks import require_aiokafka, require_aiopika, require_nats -def test_import_wrong(): - dir, app = get_app_path("tests:test_object") +def test_import_wrong() -> None: + dir, app = _get_obj_path("tests:test_object") with pytest.raises(FileNotFoundError): - import_object(dir, app) + _import_object(dir, app) @pytest.mark.parametrize( ("test_input", "exp_module", "exp_app"), - ( # noqa: PT007 + ( pytest.param( "module:app", "module", @@ -31,25 +35,25 @@ def test_import_wrong(): ), ), ) -def test_get_app_path(test_input, exp_module, exp_app): - dir, app = get_app_path(test_input) +def test_get_app_path(test_input: str, exp_module: str, exp_app: str) -> None: + dir, app = _get_obj_path(test_input) assert app == exp_app assert dir == Path.cwd() / exp_module -def test_get_app_path_wrong(): - with pytest.raises(ValueError, match="`module.app` is not a FastStream"): - get_app_path("module.app") +def test_get_app_path_wrong() -> None: + with pytest.raises(ValueError, match=r"`module.app` is not a path to object"): + _get_obj_path("module.app") -def test_import_from_string_import_wrong(): +def test_import_from_string_import_wrong() -> None: with pytest.raises(BadParameter): import_from_string("tests:test_object") @pytest.mark.parametrize( ("test_input", "exp_module"), - ( # noqa: PT007 + ( pytest.param("examples.kafka.testing:app", "examples/kafka/testing.py"), pytest.param("examples.nats.e01_basic:app", "examples/nats/e01_basic.py"), pytest.param("examples.rabbit.topic:app", "examples/rabbit/topic.py"), @@ -58,7 +62,7 @@ def test_import_from_string_import_wrong(): @require_nats @require_aiopika @require_aiokafka -def test_import_from_string(test_input, exp_module): +def test_import_from_string(test_input: str, exp_module: str) -> None: module, app = import_from_string(test_input) assert isinstance(app, FastStream) assert module == (Path.cwd() / exp_module).parent @@ -66,7 +70,7 @@ def test_import_from_string(test_input, exp_module): @pytest.mark.parametrize( ("test_input", "exp_module"), - ( # noqa: PT007 + ( pytest.param( "examples.kafka:app", "examples/kafka/__init__.py", @@ -87,12 +91,12 @@ def test_import_from_string(test_input, exp_module): @require_nats @require_aiopika @require_aiokafka -def test_import_module(test_input, exp_module): +def test_import_module(test_input: str, exp_module: str) -> None: module, app = import_from_string(test_input) assert isinstance(app, FastStream) assert module == (Path.cwd() / exp_module).parent -def test_import_from_string_wrong(): +def test_import_from_string_wrong() -> None: with pytest.raises(BadParameter): import_from_string("module.app") diff --git a/tests/cli/utils/test_parser.py b/tests/cli/utils/test_parser.py index c6bc939c01..449233a612 100644 --- a/tests/cli/utils/test_parser.py +++ b/tests/cli/utils/test_parser.py @@ -1,8 +1,6 @@ -from typing import Tuple - import pytest -from faststream.cli.utils.parser import is_bind_arg, parse_cli_args +from faststream._internal.cli.utils.parser import is_bind_arg, parse_cli_args APPLICATION = "module:app" @@ -28,18 +26,22 @@ @pytest.mark.parametrize( "args", - ( # noqa: PT007 - (APPLICATION, *ARG1, *ARG2, *ARG3, *ARG4, *ARG5, *ARG6, *ARG7, *ARG8), - (*ARG1, APPLICATION, *ARG2, *ARG3, *ARG4, *ARG5, *ARG6, *ARG7, *ARG8), - (*ARG1, *ARG2, APPLICATION, *ARG3, *ARG4, *ARG5, *ARG6, *ARG7, *ARG8), - (*ARG1, *ARG2, *ARG3, APPLICATION, *ARG4, *ARG5, *ARG6, *ARG7, *ARG8), - (*ARG1, *ARG2, *ARG3, *ARG4, APPLICATION, *ARG5, *ARG6, *ARG7, *ARG8), - (*ARG1, *ARG2, *ARG3, *ARG4, *ARG5, APPLICATION, *ARG6, *ARG7, *ARG8), - (*ARG1, *ARG2, *ARG3, *ARG4, *ARG5, *ARG6, APPLICATION, *ARG7, *ARG8), - (*ARG1, *ARG2, *ARG3, *ARG4, *ARG5, *ARG6, *ARG7, *ARG8, APPLICATION), + ( + pytest.param( + (APPLICATION, *ARG1, *ARG2, *ARG3, *ARG4, *ARG5, *ARG6, *ARG7, *ARG8), + id="app first", + ), + pytest.param( + (*ARG1, *ARG2, *ARG3, APPLICATION, *ARG4, *ARG5, *ARG6, *ARG7, *ARG8), + id="app middle", + ), + pytest.param( + (*ARG1, *ARG2, *ARG3, *ARG4, *ARG5, *ARG6, *ARG7, *ARG8, APPLICATION), + id="app last", + ), ), ) -def test_custom_argument_parsing(args: Tuple[str]): +def test_custom_argument_parsing(args: tuple[str]) -> None: app_name, extra = parse_cli_args(*args) assert app_name == APPLICATION assert extra == { @@ -55,14 +57,25 @@ def test_custom_argument_parsing(args: Tuple[str]): @pytest.mark.parametrize( - "args", ["0.0.0.0:8000", "[::]:8000", "fd://2", "unix:/tmp/socket.sock"] + "args", + ( + pytest.param("0.0.0.0:8000"), + pytest.param("[::]:8000"), + pytest.param("fd://2"), + pytest.param("unix:/tmp/socket.sock"), + ), ) def test_bind_arg(args: str): assert is_bind_arg(args) is True @pytest.mark.parametrize( - "args", ["main:app", "src.main:app", "examples.nats.e01_basic:app2"] + "args", + ( + pytest.param("main:app"), + pytest.param("src.main:app"), + pytest.param("examples.nats.e01_basic:app2"), + ), ) def test_not_bind_arg(args: str): assert is_bind_arg(args) is False diff --git a/tests/conftest.py b/tests/conftest.py index c848b5e8f4..e7c3079b36 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import asyncio -from typing import Any, Generator, List +from collections.abc import Generator +from typing import Any from unittest.mock import AsyncMock, MagicMock from uuid import uuid4 @@ -7,8 +8,7 @@ from typer.testing import CliRunner from faststream.__about__ import __version__ -from faststream.annotations import ContextRepo -from faststream.utils import context as global_context +from faststream._internal.context import ContextRepo @pytest.hookimpl(tryfirst=True) @@ -18,17 +18,17 @@ def pytest_keyboard_interrupt( pytest.mark.skip("Interrupted Test Session") -def pytest_collection_modifyitems(items: List[pytest.Item]) -> None: +def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: for item in items: item.add_marker("all") -@pytest.fixture +@pytest.fixture() def queue() -> str: return str(uuid4()) -@pytest.fixture +@pytest.fixture() def event() -> asyncio.Event: return asyncio.Event() @@ -38,15 +38,17 @@ def runner() -> CliRunner: return CliRunner() -@pytest.fixture +@pytest.fixture() def mock() -> Generator[MagicMock, Any, None]: + """Should be generator to share mock between tests.""" m = MagicMock() yield m m.reset_mock() -@pytest.fixture +@pytest.fixture() def async_mock() -> Generator[AsyncMock, Any, None]: + """Should be generator to share mock between tests.""" m = AsyncMock() yield m m.reset_mock() @@ -57,12 +59,16 @@ def version() -> str: return __version__ -@pytest.fixture -def context() -> Generator[ContextRepo, Any, None]: - yield global_context - global_context.clear() +@pytest.fixture() +def context() -> ContextRepo: + return ContextRepo() -@pytest.fixture +@pytest.fixture() def kafka_basic_project() -> str: return "docs.docs_src.kafka.basic.basic:app" + + +@pytest.fixture() +def kafka_ascynapi_project() -> str: + return "docs.docs_src.kafka.basic.basic:asyncapi" diff --git a/tests/a_docs/confluent/basic/__init__.py b/tests/docs/__init__.py similarity index 100% rename from tests/a_docs/confluent/basic/__init__.py rename to tests/docs/__init__.py diff --git a/tests/docs/confluent/__init__.py b/tests/docs/confluent/__init__.py new file mode 100644 index 0000000000..c4a1803708 --- /dev/null +++ b/tests/docs/confluent/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("confluent_kafka") diff --git a/tests/a_docs/confluent/batch_consuming_pydantic/__init__.py b/tests/docs/confluent/ack/__init__.py similarity index 100% rename from tests/a_docs/confluent/batch_consuming_pydantic/__init__.py rename to tests/docs/confluent/ack/__init__.py diff --git a/tests/docs/confluent/ack/test_errors.py b/tests/docs/confluent/ack/test_errors.py new file mode 100644 index 0000000000..ba2925f269 --- /dev/null +++ b/tests/docs/confluent/ack/test_errors.py @@ -0,0 +1,15 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from faststream.confluent import TestApp, TestKafkaBroker +from faststream.confluent.message import KafkaMessage + + +@pytest.mark.asyncio() +async def test_ack_exc(mock: MagicMock) -> None: + from docs.docs_src.confluent.ack.errors import app, broker + + with patch.object(KafkaMessage, "ack", mock): + async with TestKafkaBroker(broker), TestApp(app): + mock.assert_called_once() diff --git a/tests/a_docs/confluent/consumes_basics/__init__.py b/tests/docs/confluent/additional_config/__init__.py similarity index 100% rename from tests/a_docs/confluent/consumes_basics/__init__.py rename to tests/docs/confluent/additional_config/__init__.py diff --git a/tests/docs/confluent/additional_config/test_app.py b/tests/docs/confluent/additional_config/test_app.py new file mode 100644 index 0000000000..6756792d0f --- /dev/null +++ b/tests/docs/confluent/additional_config/test_app.py @@ -0,0 +1,15 @@ +import pytest + +from docs.docs_src.confluent.additional_config.app import ( + HelloWorld, + broker, + on_hello_world, +) +from faststream.confluent import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_base_app() -> None: + async with TestKafkaBroker(broker): + await broker.publish(HelloWorld(msg="First Hello"), "hello_world") + on_hello_world.mock.assert_called_with(dict(HelloWorld(msg="First Hello"))) diff --git a/tests/a_docs/confluent/publish_batch/__init__.py b/tests/docs/confluent/basic/__init__.py similarity index 100% rename from tests/a_docs/confluent/publish_batch/__init__.py rename to tests/docs/confluent/basic/__init__.py diff --git a/tests/docs/confluent/basic/test_basic.py b/tests/docs/confluent/basic/test_basic.py new file mode 100644 index 0000000000..014065408b --- /dev/null +++ b/tests/docs/confluent/basic/test_basic.py @@ -0,0 +1,15 @@ +import pytest + +from faststream.confluent import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_basic() -> None: + from docs.docs_src.confluent.basic.basic import broker, on_input_data + + publisher = broker._publishers[0] + + async with TestKafkaBroker(broker) as br: + await br.publish({"data": 1.0}, "input_data") + on_input_data.mock.assert_called_once_with({"data": 1.0}) + publisher.mock.assert_called_once_with({"data": 2.0}) diff --git a/tests/docs/confluent/basic/test_cmd_run.py b/tests/docs/confluent/basic/test_cmd_run.py new file mode 100644 index 0000000000..cbddc87691 --- /dev/null +++ b/tests/docs/confluent/basic/test_cmd_run.py @@ -0,0 +1,38 @@ +from unittest.mock import MagicMock + +import pytest +from typer.testing import CliRunner + +from faststream._internal.cli.main import cli +from faststream.app import FastStream + + +@pytest.fixture() +def confluent_basic_project() -> str: + return "docs.docs_src.confluent.basic.basic:app" + + +@pytest.mark.confluent() +def test_run_cmd( + runner: CliRunner, + mock: MagicMock, + monkeypatch: pytest.MonkeyPatch, + confluent_basic_project: str, +) -> None: + async def patched_run(self: FastStream, *args, **kwargs) -> None: + await self.start() + await self.stop() + mock() + + with monkeypatch.context() as m: + m.setattr(FastStream, "run", patched_run) + r = runner.invoke( + cli, + [ + "run", + confluent_basic_project, + ], + ) + + assert r.exit_code == 0 + mock.assert_called_once() diff --git a/tests/a_docs/confluent/publish_example/__init__.py b/tests/docs/confluent/batch_consuming_pydantic/__init__.py similarity index 100% rename from tests/a_docs/confluent/publish_example/__init__.py rename to tests/docs/confluent/batch_consuming_pydantic/__init__.py diff --git a/tests/docs/confluent/batch_consuming_pydantic/test_app.py b/tests/docs/confluent/batch_consuming_pydantic/test_app.py new file mode 100644 index 0000000000..95474417d9 --- /dev/null +++ b/tests/docs/confluent/batch_consuming_pydantic/test_app.py @@ -0,0 +1,21 @@ +import pytest + +from docs.docs_src.confluent.batch_consuming_pydantic.app import ( + HelloWorld, + broker, + handle_batch, +) +from faststream.confluent import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_me() -> None: + async with TestKafkaBroker(broker): + await broker.publish_batch( + HelloWorld(msg="First Hello"), + HelloWorld(msg="Second Hello"), + topic="test_batch", + ) + handle_batch.mock.assert_called_with( + [dict(HelloWorld(msg="First Hello")), dict(HelloWorld(msg="Second Hello"))], + ) diff --git a/tests/a_docs/confluent/publish_with_partition_key/__init__.py b/tests/docs/confluent/consumes_basics/__init__.py similarity index 100% rename from tests/a_docs/confluent/publish_with_partition_key/__init__.py rename to tests/docs/confluent/consumes_basics/__init__.py diff --git a/tests/docs/confluent/consumes_basics/test_app.py b/tests/docs/confluent/consumes_basics/test_app.py new file mode 100644 index 0000000000..c79885711b --- /dev/null +++ b/tests/docs/confluent/consumes_basics/test_app.py @@ -0,0 +1,15 @@ +import pytest + +from docs.docs_src.confluent.consumes_basics.app import ( + HelloWorld, + broker, + on_hello_world, +) +from faststream.confluent import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_base_app() -> None: + async with TestKafkaBroker(broker): + await broker.publish(HelloWorld(msg="First Hello"), "hello_world") + on_hello_world.mock.assert_called_with(dict(HelloWorld(msg="First Hello"))) diff --git a/tests/a_docs/confluent/publisher_object/__init__.py b/tests/docs/confluent/publish_batch/__init__.py similarity index 100% rename from tests/a_docs/confluent/publisher_object/__init__.py rename to tests/docs/confluent/publish_batch/__init__.py diff --git a/tests/docs/confluent/publish_batch/test_app.py b/tests/docs/confluent/publish_batch/test_app.py new file mode 100644 index 0000000000..d8042ba94d --- /dev/null +++ b/tests/docs/confluent/publish_batch/test_app.py @@ -0,0 +1,32 @@ +import pytest + +from docs.docs_src.confluent.publish_batch.app import ( + Data, + broker, + decrease_and_increase, + on_input_data_1, + on_input_data_2, +) +from faststream.confluent import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_batch_publish_decorator() -> None: + async with TestKafkaBroker(broker): + await broker.publish(Data(data=2.0), "input_data_1") + + on_input_data_1.mock.assert_called_once_with(dict(Data(data=2.0))) + decrease_and_increase.mock.assert_called_once_with( + [dict(Data(data=1.0)), dict(Data(data=4.0))], + ) + + +@pytest.mark.asyncio() +async def test_batch_publish_call() -> None: + async with TestKafkaBroker(broker): + await broker.publish(Data(data=2.0), "input_data_2") + + on_input_data_2.mock.assert_called_once_with(dict(Data(data=2.0))) + decrease_and_increase.mock.assert_called_once_with( + [dict(Data(data=1.0)), dict(Data(data=4.0))], + ) diff --git a/tests/docs/confluent/publish_batch/test_issues.py b/tests/docs/confluent/publish_batch/test_issues.py new file mode 100644 index 0000000000..3c511da0d8 --- /dev/null +++ b/tests/docs/confluent/publish_batch/test_issues.py @@ -0,0 +1,22 @@ +import pytest + +from faststream import FastStream +from faststream.confluent import KafkaBroker, TestKafkaBroker + +broker = KafkaBroker() +batch_producer = broker.publisher("response", batch=True) + + +@batch_producer +@broker.subscriber("test") +async def handle(msg: str) -> list[int]: + return [1, 2, 3] + + +app = FastStream(broker) + + +@pytest.mark.asyncio() +async def test_base_app() -> None: + async with TestKafkaBroker(broker): + await broker.publish("", "test") diff --git a/tests/a_docs/confluent/raw_publish/__init__.py b/tests/docs/confluent/publish_example/__init__.py similarity index 100% rename from tests/a_docs/confluent/raw_publish/__init__.py rename to tests/docs/confluent/publish_example/__init__.py diff --git a/tests/docs/confluent/publish_example/test_app.py b/tests/docs/confluent/publish_example/test_app.py new file mode 100644 index 0000000000..beb23d4b36 --- /dev/null +++ b/tests/docs/confluent/publish_example/test_app.py @@ -0,0 +1,18 @@ +import pytest + +from docs.docs_src.confluent.publish_example.app import ( + Data, + broker, + on_input_data, + to_output_data, +) +from faststream.confluent import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_base_app() -> None: + async with TestKafkaBroker(broker): + await broker.publish(Data(data=0.2), "input_data") + + on_input_data.mock.assert_called_once_with(dict(Data(data=0.2))) + to_output_data.mock.assert_called_once_with(dict(Data(data=1.2))) diff --git a/tests/a_docs/getting_started/__init__.py b/tests/docs/confluent/publish_with_partition_key/__init__.py similarity index 100% rename from tests/a_docs/getting_started/__init__.py rename to tests/docs/confluent/publish_with_partition_key/__init__.py diff --git a/tests/docs/confluent/publish_with_partition_key/test_app.py b/tests/docs/confluent/publish_with_partition_key/test_app.py new file mode 100644 index 0000000000..517bd7940c --- /dev/null +++ b/tests/docs/confluent/publish_with_partition_key/test_app.py @@ -0,0 +1,30 @@ +import pytest + +from docs.docs_src.confluent.publish_with_partition_key.app import ( + Data, + broker, + on_input_data, + to_output_data, +) +from faststream.confluent import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_app() -> None: + async with TestKafkaBroker(broker): + await broker.publish(Data(data=0.2), "input_data", key=b"my_key") + + on_input_data.mock.assert_called_once_with(dict(Data(data=0.2))) + to_output_data.mock.assert_called_once_with(dict(Data(data=1.2))) + + +@pytest.mark.skip("we are not checking the key") +@pytest.mark.asyncio() +async def test_keys() -> None: + async with TestKafkaBroker(broker): + # we should be able to publish a message with the key + await broker.publish(Data(data=0.2), "input_data", key=b"my_key") + + # we need to check the key as well + on_input_data.mock.assert_called_once_with(dict(Data(data=0.2)), key=b"my_key") + to_output_data.mock.assert_called_once_with(dict(Data(data=1.2)), key=b"key") diff --git a/tests/a_docs/getting_started/asyncapi/__init__.py b/tests/docs/confluent/publisher_object/__init__.py similarity index 100% rename from tests/a_docs/getting_started/asyncapi/__init__.py rename to tests/docs/confluent/publisher_object/__init__.py diff --git a/tests/a_docs/confluent/publisher_object/test_publisher_object.py b/tests/docs/confluent/publisher_object/test_publisher_object.py similarity index 100% rename from tests/a_docs/confluent/publisher_object/test_publisher_object.py rename to tests/docs/confluent/publisher_object/test_publisher_object.py diff --git a/tests/a_docs/getting_started/cli/__init__.py b/tests/docs/confluent/raw_publish/__init__.py similarity index 100% rename from tests/a_docs/getting_started/cli/__init__.py rename to tests/docs/confluent/raw_publish/__init__.py diff --git a/tests/a_docs/confluent/raw_publish/test_raw_publish.py b/tests/docs/confluent/raw_publish/test_raw_publish.py similarity index 100% rename from tests/a_docs/confluent/raw_publish/test_raw_publish.py rename to tests/docs/confluent/raw_publish/test_raw_publish.py diff --git a/tests/docs/confluent/test_security.py b/tests/docs/confluent/test_security.py new file mode 100644 index 0000000000..9319e38bf2 --- /dev/null +++ b/tests/docs/confluent/test_security.py @@ -0,0 +1,89 @@ +import pytest +from dirty_equals import IsPartialDict + + +@pytest.mark.asyncio() +@pytest.mark.confluent() +async def test_base_security() -> None: + from docs.docs_src.confluent.security.basic import broker as basic_broker + + assert basic_broker.config.connection_config.producer_config == IsPartialDict({ + "security.protocol": "ssl" + }) + + +@pytest.mark.asyncio() +@pytest.mark.confluent() +async def test_scram256() -> None: + from docs.docs_src.confluent.security.sasl_scram256 import ( + broker as scram256_broker, + ) + + assert scram256_broker.config.connection_config.producer_config == IsPartialDict({ + "sasl.mechanism": "SCRAM-SHA-256", + "sasl.username": "admin", + "sasl.password": "password", # pragma: allowlist secret + "security.protocol": "sasl_ssl", + }) + + +@pytest.mark.asyncio() +@pytest.mark.confluent() +async def test_scram512() -> None: + from docs.docs_src.confluent.security.sasl_scram512 import ( + broker as scram512_broker, + ) + + assert scram512_broker.config.connection_config.producer_config == IsPartialDict({ + "sasl.mechanism": "SCRAM-SHA-512", + "sasl.username": "admin", + "sasl.password": "password", # pragma: allowlist secret + "security.protocol": "sasl_ssl", + }) + + +@pytest.mark.asyncio() +@pytest.mark.confluent() +async def test_plaintext() -> None: + from docs.docs_src.confluent.security.plaintext import ( + broker as plaintext_broker, + ) + + producer_config = plaintext_broker.config.connection_config.producer_config + + assert producer_config == IsPartialDict({ + "sasl.mechanism": "PLAIN", + "sasl.username": "admin", + "sasl.password": "password", # pragma: allowlist secret + "security.protocol": "sasl_ssl", + }) + + +@pytest.mark.asyncio() +@pytest.mark.confluent() +async def test_oathbearer() -> None: + from docs.docs_src.confluent.security.sasl_oauthbearer import ( + broker as oauthbearer_broker, + ) + + producer_config = oauthbearer_broker.config.connection_config.producer_config + + assert producer_config == IsPartialDict({ + "sasl.mechanism": "OAUTHBEARER", + "security.protocol": "sasl_ssl", + }) + + +@pytest.mark.asyncio() +@pytest.mark.confluent() +async def test_gssapi() -> None: + from docs.docs_src.confluent.security.sasl_gssapi import ( + broker as gssapi_broker, + ) + + producer_config = gssapi_broker.config.connection_config.producer_config + + assert producer_config == IsPartialDict({ + "sasl.mechanism": "GSSAPI", + "security.protocol": "sasl_ssl", + }) diff --git a/tests/a_docs/getting_started/context/__init__.py b/tests/docs/getting_started/__init__.py similarity index 100% rename from tests/a_docs/getting_started/context/__init__.py rename to tests/docs/getting_started/__init__.py diff --git a/tests/a_docs/getting_started/dependencies/__init__.py b/tests/docs/getting_started/asyncapi/__init__.py similarity index 100% rename from tests/a_docs/getting_started/dependencies/__init__.py rename to tests/docs/getting_started/asyncapi/__init__.py diff --git a/tests/a_docs/getting_started/cli/kafka/__init__.py b/tests/docs/getting_started/asyncapi/asyncapi_customization/__init__.py similarity index 100% rename from tests/a_docs/getting_started/cli/kafka/__init__.py rename to tests/docs/getting_started/asyncapi/asyncapi_customization/__init__.py diff --git a/tests/docs/getting_started/asyncapi/asyncapi_customization/test_basic.py b/tests/docs/getting_started/asyncapi/asyncapi_customization/test_basic.py new file mode 100644 index 0000000000..05577fbbb2 --- /dev/null +++ b/tests/docs/getting_started/asyncapi/asyncapi_customization/test_basic.py @@ -0,0 +1,65 @@ +from docs.docs_src.getting_started.asyncapi.asyncapi_customization.basic import app +from faststream.specification.asyncapi import AsyncAPI + + +def test_basic_customization() -> None: + schema = AsyncAPI(app.broker, schema_version="2.6.0").to_jsonable() + + assert schema == { + "asyncapi": "2.6.0", + "channels": { + "input_data:OnInputData": { + "bindings": { + "kafka": {"bindingVersion": "0.4.0", "topic": "input_data"}, + }, + "servers": ["development"], + "publish": { + "message": { + "$ref": "#/components/messages/input_data:OnInputData:Message", + }, + }, + }, + "output_data:Publisher": { + "bindings": { + "kafka": {"bindingVersion": "0.4.0", "topic": "output_data"}, + }, + "subscribe": { + "message": { + "$ref": "#/components/messages/output_data:Publisher:Message", + }, + }, + "servers": ["development"], + }, + }, + "components": { + "messages": { + "input_data:OnInputData:Message": { + "correlationId": {"location": "$message.header#/correlation_id"}, + "payload": { + "$ref": "#/components/schemas/OnInputData:Message:Payload", + }, + "title": "input_data:OnInputData:Message", + }, + "output_data:Publisher:Message": { + "correlationId": {"location": "$message.header#/correlation_id"}, + "payload": { + "$ref": "#/components/schemas/output_data:PublisherPayload", + }, + "title": "output_data:Publisher:Message", + }, + }, + "schemas": { + "OnInputData:Message:Payload": {"title": "OnInputData:Message:Payload"}, + "output_data:PublisherPayload": {}, + }, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "kafka", + "protocolVersion": "auto", + "url": "localhost:9092", + }, + }, + } diff --git a/tests/docs/getting_started/asyncapi/asyncapi_customization/test_broker.py b/tests/docs/getting_started/asyncapi/asyncapi_customization/test_broker.py new file mode 100644 index 0000000000..155786f76d --- /dev/null +++ b/tests/docs/getting_started/asyncapi/asyncapi_customization/test_broker.py @@ -0,0 +1,16 @@ +from docs.docs_src.getting_started.asyncapi.asyncapi_customization.custom_broker import ( + asyncapi, +) + + +def test_broker_customization() -> None: + schema = asyncapi.to_jsonable() + + assert schema["servers"] == { + "development": { + "url": "non-sensitive-url:9092", + "protocol": "kafka", + "description": "Kafka broker running locally", + "protocolVersion": "auto", + }, + } diff --git a/tests/docs/getting_started/asyncapi/asyncapi_customization/test_handler.py b/tests/docs/getting_started/asyncapi/asyncapi_customization/test_handler.py new file mode 100644 index 0000000000..d697b457f0 --- /dev/null +++ b/tests/docs/getting_started/asyncapi/asyncapi_customization/test_handler.py @@ -0,0 +1,38 @@ +from dirty_equals import IsPartialDict + +from docs.docs_src.getting_started.asyncapi.asyncapi_customization.custom_handler import ( + asyncapi, +) + + +def test_handler_customization() -> None: + schema = asyncapi.to_jsonable() + + (subscriber_key, subscriber_value), (publisher_key, publisher_value) = schema[ + "channels" + ].items() + + assert subscriber_key == "input_data:Consume", subscriber_key + assert subscriber_value == IsPartialDict({ + "servers": ["development"], + "bindings": {"kafka": {"topic": "input_data", "bindingVersion": "0.4.0"}}, + "publish": { + "message": {"$ref": "#/components/messages/input_data:Consume:Message"}, + }, + }), subscriber_value + desc = subscriber_value["description"] + assert ( # noqa: PT018 + "Consumer function\n\n" in desc + and "Args:\n" in desc + and " msg: input msg" in desc + ), desc + + assert publisher_key == "output_data:Produce", publisher_key + assert publisher_value == { + "description": "My publisher description", + "servers": ["development"], + "bindings": {"kafka": {"topic": "output_data", "bindingVersion": "0.4.0"}}, + "subscribe": { + "message": {"$ref": "#/components/messages/output_data:Produce:Message"} + }, + } diff --git a/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_info.py b/tests/docs/getting_started/asyncapi/asyncapi_customization/test_info.py similarity index 77% rename from tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_info.py rename to tests/docs/getting_started/asyncapi/asyncapi_customization/test_info.py index 7d7a02a886..f00f2eecd6 100644 --- a/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_info.py +++ b/tests/docs/getting_started/asyncapi/asyncapi_customization/test_info.py @@ -1,11 +1,10 @@ from docs.docs_src.getting_started.asyncapi.asyncapi_customization.custom_info import ( - app, + asyncapi, ) -from faststream.asyncapi.generate import get_app_schema -def test_info_customization(): - schema = get_app_schema(app).to_jsonable() +def test_info_customization() -> None: + schema = asyncapi.to_jsonable() assert schema["info"] == { "title": "My App", diff --git a/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_payload.py b/tests/docs/getting_started/asyncapi/asyncapi_customization/test_payload.py similarity index 75% rename from tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_payload.py rename to tests/docs/getting_started/asyncapi/asyncapi_customization/test_payload.py index 5b4d693321..4639b51cbf 100644 --- a/tests/a_docs/getting_started/asyncapi/asyncapi_customization/test_payload.py +++ b/tests/docs/getting_started/asyncapi/asyncapi_customization/test_payload.py @@ -1,11 +1,10 @@ from docs.docs_src.getting_started.asyncapi.asyncapi_customization.payload_info import ( - app, + asyncapi, ) -from faststream.asyncapi.generate import get_app_schema -def test_payload_customization(): - schema = get_app_schema(app).to_jsonable() +def test_payload_customization() -> None: + schema = asyncapi.to_jsonable() assert schema["components"]["schemas"] == { "DataBasic": { @@ -16,10 +15,10 @@ def test_payload_customization(): "minimum": 0, "title": "Data", "type": "number", - } + }, }, "required": ["data"], "title": "DataBasic", "type": "object", - } + }, } diff --git a/tests/a_docs/getting_started/dependencies/basic/__init__.py b/tests/docs/getting_started/cli/__init__.py similarity index 100% rename from tests/a_docs/getting_started/dependencies/basic/__init__.py rename to tests/docs/getting_started/cli/__init__.py diff --git a/tests/docs/getting_started/cli/confluent/__init__.py b/tests/docs/getting_started/cli/confluent/__init__.py new file mode 100644 index 0000000000..c4a1803708 --- /dev/null +++ b/tests/docs/getting_started/cli/confluent/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("confluent_kafka") diff --git a/tests/docs/getting_started/cli/confluent/test_confluent_context.py b/tests/docs/getting_started/cli/confluent/test_confluent_context.py new file mode 100644 index 0000000000..64f2a331e4 --- /dev/null +++ b/tests/docs/getting_started/cli/confluent/test_confluent_context.py @@ -0,0 +1,16 @@ +import pytest + +from faststream import TestApp +from faststream.confluent import TestKafkaBroker +from tests.marks import pydantic_v2 +from tests.mocks import mock_pydantic_settings_env + + +@pydantic_v2 +@pytest.mark.asyncio() +async def test() -> None: + with mock_pydantic_settings_env({"any_flag": "True"}): + from docs.docs_src.getting_started.cli.confluent_context import app, broker + + async with TestKafkaBroker(broker), TestApp(app, {"env": ""}): + assert app.context.get("settings").any_flag diff --git a/tests/a_docs/kafka/__init__.py b/tests/docs/getting_started/cli/kafka/__init__.py similarity index 100% rename from tests/a_docs/kafka/__init__.py rename to tests/docs/getting_started/cli/kafka/__init__.py diff --git a/tests/docs/getting_started/cli/kafka/test_kafka_context.py b/tests/docs/getting_started/cli/kafka/test_kafka_context.py new file mode 100644 index 0000000000..2c7c096bd6 --- /dev/null +++ b/tests/docs/getting_started/cli/kafka/test_kafka_context.py @@ -0,0 +1,16 @@ +import pytest + +from faststream import TestApp +from faststream.kafka import TestKafkaBroker +from tests.marks import pydantic_v2 +from tests.mocks import mock_pydantic_settings_env + + +@pydantic_v2 +@pytest.mark.asyncio() +async def test() -> None: + with mock_pydantic_settings_env({"any_flag": "True"}): + from docs.docs_src.getting_started.cli.kafka_context import app, broker + + async with TestKafkaBroker(broker), TestApp(app, {"env": ""}): + assert app.context.get("settings").any_flag diff --git a/tests/docs/getting_started/cli/nats/__init__.py b/tests/docs/getting_started/cli/nats/__init__.py new file mode 100644 index 0000000000..87ead90ee6 --- /dev/null +++ b/tests/docs/getting_started/cli/nats/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("nats") diff --git a/tests/docs/getting_started/cli/nats/test_nats_context.py b/tests/docs/getting_started/cli/nats/test_nats_context.py new file mode 100644 index 0000000000..d754efff6c --- /dev/null +++ b/tests/docs/getting_started/cli/nats/test_nats_context.py @@ -0,0 +1,16 @@ +import pytest + +from faststream import TestApp +from faststream.nats import TestNatsBroker +from tests.marks import pydantic_v2 +from tests.mocks import mock_pydantic_settings_env + + +@pydantic_v2 +@pytest.mark.asyncio() +async def test() -> None: + with mock_pydantic_settings_env({"any_flag": "True"}): + from docs.docs_src.getting_started.cli.nats_context import app, broker + + async with TestNatsBroker(broker), TestApp(app, {"env": ""}): + assert app.context.get("settings").any_flag diff --git a/tests/docs/getting_started/cli/rabbit/__init__.py b/tests/docs/getting_started/cli/rabbit/__init__.py new file mode 100644 index 0000000000..ebec43fcd5 --- /dev/null +++ b/tests/docs/getting_started/cli/rabbit/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("aio_pika") diff --git a/tests/docs/getting_started/cli/rabbit/test_rabbit_context.py b/tests/docs/getting_started/cli/rabbit/test_rabbit_context.py new file mode 100644 index 0000000000..afea0e29ab --- /dev/null +++ b/tests/docs/getting_started/cli/rabbit/test_rabbit_context.py @@ -0,0 +1,16 @@ +import pytest + +from faststream import TestApp +from faststream.rabbit import TestRabbitBroker +from tests.marks import pydantic_v2 +from tests.mocks import mock_pydantic_settings_env + + +@pydantic_v2 +@pytest.mark.asyncio() +async def test() -> None: + with mock_pydantic_settings_env({"any_flag": "True"}): + from docs.docs_src.getting_started.cli.rabbit_context import app, broker + + async with TestRabbitBroker(broker), TestApp(app, {"env": ".env"}): + assert app.context.get("settings").any_flag diff --git a/tests/docs/getting_started/cli/redis/__init__.py b/tests/docs/getting_started/cli/redis/__init__.py new file mode 100644 index 0000000000..4752ef19b1 --- /dev/null +++ b/tests/docs/getting_started/cli/redis/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("redis") diff --git a/tests/docs/getting_started/cli/redis/test_redis_context.py b/tests/docs/getting_started/cli/redis/test_redis_context.py new file mode 100644 index 0000000000..a9debc3ebb --- /dev/null +++ b/tests/docs/getting_started/cli/redis/test_redis_context.py @@ -0,0 +1,16 @@ +import pytest + +from faststream import TestApp +from faststream.redis import TestRedisBroker +from tests.marks import pydantic_v2 +from tests.mocks import mock_pydantic_settings_env + + +@pydantic_v2 +@pytest.mark.asyncio() +async def test() -> None: + with mock_pydantic_settings_env({"any_flag": "True"}): + from docs.docs_src.getting_started.cli.redis_context import app, broker + + async with TestRedisBroker(broker), TestApp(app, {"env": ".env"}): + assert app.context.get("settings").any_flag diff --git a/tests/a_docs/getting_started/index/__init__.py b/tests/docs/getting_started/config/__init__.py similarity index 100% rename from tests/a_docs/getting_started/index/__init__.py rename to tests/docs/getting_started/config/__init__.py diff --git a/tests/a_docs/getting_started/config/test_settings_base_1.py b/tests/docs/getting_started/config/test_settings_base_1.py similarity index 82% rename from tests/a_docs/getting_started/config/test_settings_base_1.py rename to tests/docs/getting_started/config/test_settings_base_1.py index fd42ba6533..66927a45a4 100644 --- a/tests/a_docs/getting_started/config/test_settings_base_1.py +++ b/tests/docs/getting_started/config/test_settings_base_1.py @@ -2,7 +2,7 @@ @pydantic_v1 -def test_exists_and_valid(): +def test_exists_and_valid() -> None: from docs.docs_src.getting_started.config.settings_base_1 import settings assert settings.queue == "test-queue" diff --git a/tests/a_docs/getting_started/config/test_settings_base_2.py b/tests/docs/getting_started/config/test_settings_base_2.py similarity index 90% rename from tests/a_docs/getting_started/config/test_settings_base_2.py rename to tests/docs/getting_started/config/test_settings_base_2.py index 780f73f278..584a746ef9 100644 --- a/tests/a_docs/getting_started/config/test_settings_base_2.py +++ b/tests/docs/getting_started/config/test_settings_base_2.py @@ -3,7 +3,7 @@ @pydantic_v2 -def test_exists_and_valid(): +def test_exists_and_valid() -> None: with mock_pydantic_settings_env({"url": "localhost:9092"}): from docs.docs_src.getting_started.config.settings_base_2 import settings diff --git a/tests/a_docs/getting_started/config/test_settings_env.py b/tests/docs/getting_started/config/test_settings_env.py similarity index 90% rename from tests/a_docs/getting_started/config/test_settings_env.py rename to tests/docs/getting_started/config/test_settings_env.py index 960485ed4c..bc3efbe3d2 100644 --- a/tests/a_docs/getting_started/config/test_settings_env.py +++ b/tests/docs/getting_started/config/test_settings_env.py @@ -3,7 +3,7 @@ @pydantic_v2 -def test_exists_and_valid(): +def test_exists_and_valid() -> None: with mock_pydantic_settings_env({"url": "localhost:9092"}): from docs.docs_src.getting_started.config.settings_env import settings diff --git a/tests/a_docs/getting_started/config/test_usage.py b/tests/docs/getting_started/config/test_usage.py similarity index 90% rename from tests/a_docs/getting_started/config/test_usage.py rename to tests/docs/getting_started/config/test_usage.py index 2ae34dda33..6459f1d1d3 100644 --- a/tests/a_docs/getting_started/config/test_usage.py +++ b/tests/docs/getting_started/config/test_usage.py @@ -4,7 +4,7 @@ @pydantic_v2 @require_aiopika -def test_exists_and_valid(): +def test_exists_and_valid() -> None: with mock_pydantic_settings_env({"url": "localhost:9092"}): from docs.docs_src.getting_started.config.usage import settings diff --git a/tests/a_docs/getting_started/lifespan/__init__.py b/tests/docs/getting_started/context/__init__.py similarity index 100% rename from tests/a_docs/getting_started/lifespan/__init__.py rename to tests/docs/getting_started/context/__init__.py diff --git a/tests/docs/getting_started/context/test_annotated.py b/tests/docs/getting_started/context/test_annotated.py new file mode 100644 index 0000000000..5f1848ff29 --- /dev/null +++ b/tests/docs/getting_started/context/test_annotated.py @@ -0,0 +1,84 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_annotated_kafka() -> None: + from docs.docs_src.getting_started.context.kafka.annotated import ( + base_handler, + broker, + ) + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker) as br: + await br.publish("Hi!", "test") + + base_handler.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_confluent +async def test_annotated_confluent() -> None: + from docs.docs_src.getting_started.context.confluent.annotated import ( + base_handler, + broker, + ) + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker) as br: + await br.publish("Hi!", "test") + + base_handler.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_aiopika +async def test_annotated_rabbit() -> None: + from docs.docs_src.getting_started.context.rabbit.annotated import ( + base_handler, + broker, + ) + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker) as br: + await br.publish("Hi!", "test") + + base_handler.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_nats +async def test_annotated_nats() -> None: + from docs.docs_src.getting_started.context.nats.annotated import ( + base_handler, + broker, + ) + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker) as br: + await br.publish("Hi!", "test") + + base_handler.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_redis +async def test_annotated_redis() -> None: + from docs.docs_src.getting_started.context.redis.annotated import ( + base_handler, + broker, + ) + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker) as br: + await br.publish("Hi!", "test") + + base_handler.mock.assert_called_once_with("Hi!") diff --git a/tests/docs/getting_started/context/test_base.py b/tests/docs/getting_started/context/test_base.py new file mode 100644 index 0000000000..8785fbb7bc --- /dev/null +++ b/tests/docs/getting_started/context/test_base.py @@ -0,0 +1,72 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_base_kafka() -> None: + from docs.docs_src.getting_started.context.kafka.base import base_handler, broker + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker) as br: + await br.publish("Hi!", "test") + + base_handler.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_confluent +async def test_base_confluent() -> None: + from docs.docs_src.getting_started.context.confluent.base import ( + base_handler, + broker, + ) + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker) as br: + await br.publish("Hi!", "test") + + base_handler.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_aiopika +async def test_base_rabbit() -> None: + from docs.docs_src.getting_started.context.rabbit.base import base_handler, broker + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker) as br: + await br.publish("Hi!", "test") + + base_handler.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_nats +async def test_base_nats() -> None: + from docs.docs_src.getting_started.context.nats.base import base_handler, broker + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker) as br: + await br.publish("Hi!", "test") + + base_handler.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_redis +async def test_base_redis() -> None: + from docs.docs_src.getting_started.context.redis.base import base_handler, broker + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker) as br: + await br.publish("Hi!", "test") + + base_handler.mock.assert_called_once_with("Hi!") diff --git a/tests/a_docs/getting_started/context/test_cast.py b/tests/docs/getting_started/context/test_cast.py similarity index 88% rename from tests/a_docs/getting_started/context/test_cast.py rename to tests/docs/getting_started/context/test_cast.py index 33cbfbedcc..ebc15b0579 100644 --- a/tests/a_docs/getting_started/context/test_cast.py +++ b/tests/docs/getting_started/context/test_cast.py @@ -9,9 +9,9 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_cast_kafka(): +async def test_cast_kafka() -> None: from docs.docs_src.getting_started.context.kafka.cast import ( broker, handle, @@ -29,9 +29,9 @@ async def test_cast_kafka(): handle_int.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_cast_confluent(): +async def test_cast_confluent() -> None: from docs.docs_src.getting_started.context.confluent.cast import ( broker, handle, @@ -49,9 +49,9 @@ async def test_cast_confluent(): handle_int.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_cast_rabbit(): +async def test_cast_rabbit() -> None: from docs.docs_src.getting_started.context.rabbit.cast import ( broker, handle, @@ -69,9 +69,9 @@ async def test_cast_rabbit(): handle_int.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_cast_nats(): +async def test_cast_nats() -> None: from docs.docs_src.getting_started.context.nats.cast import ( broker, handle, @@ -89,9 +89,9 @@ async def test_cast_nats(): handle_int.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_cast_redis(): +async def test_cast_redis() -> None: from docs.docs_src.getting_started.context.redis.cast import ( broker, handle, diff --git a/tests/a_docs/getting_started/context/test_custom_global.py b/tests/docs/getting_started/context/test_custom_global.py similarity index 83% rename from tests/a_docs/getting_started/context/test_custom_global.py rename to tests/docs/getting_started/context/test_custom_global.py index 1089195a20..80a996e7d9 100644 --- a/tests/a_docs/getting_started/context/test_custom_global.py +++ b/tests/docs/getting_started/context/test_custom_global.py @@ -10,9 +10,9 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_custom_global_context_kafka(): +async def test_custom_global_context_kafka() -> None: from docs.docs_src.getting_started.context.kafka.custom_global_context import ( app, broker, @@ -26,9 +26,9 @@ async def test_custom_global_context_kafka(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_custom_global_context_confluent(): +async def test_custom_global_context_confluent() -> None: from docs.docs_src.getting_started.context.confluent.custom_global_context import ( app, broker, @@ -42,9 +42,9 @@ async def test_custom_global_context_confluent(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_custom_global_context_rabbit(): +async def test_custom_global_context_rabbit() -> None: from docs.docs_src.getting_started.context.rabbit.custom_global_context import ( app, broker, @@ -58,9 +58,9 @@ async def test_custom_global_context_rabbit(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_custom_global_context_nats(): +async def test_custom_global_context_nats() -> None: from docs.docs_src.getting_started.context.nats.custom_global_context import ( app, broker, @@ -74,9 +74,9 @@ async def test_custom_global_context_nats(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_custom_global_context_redis(): +async def test_custom_global_context_redis() -> None: from docs.docs_src.getting_started.context.redis.custom_global_context import ( app, broker, diff --git a/tests/a_docs/getting_started/context/test_custom_local.py b/tests/docs/getting_started/context/test_custom_local.py similarity index 82% rename from tests/a_docs/getting_started/context/test_custom_local.py rename to tests/docs/getting_started/context/test_custom_local.py index 4761aa3b5a..a1fce85f4c 100644 --- a/tests/a_docs/getting_started/context/test_custom_local.py +++ b/tests/docs/getting_started/context/test_custom_local.py @@ -9,9 +9,9 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_custom_local_context_kafka(): +async def test_custom_local_context_kafka() -> None: from docs.docs_src.getting_started.context.kafka.custom_local_context import ( broker, handle, @@ -24,9 +24,9 @@ async def test_custom_local_context_kafka(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_custom_local_context_confluent(): +async def test_custom_local_context_confluent() -> None: from docs.docs_src.getting_started.context.confluent.custom_local_context import ( broker, handle, @@ -39,9 +39,9 @@ async def test_custom_local_context_confluent(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_custom_local_context_rabbit(): +async def test_custom_local_context_rabbit() -> None: from docs.docs_src.getting_started.context.rabbit.custom_local_context import ( broker, handle, @@ -54,9 +54,9 @@ async def test_custom_local_context_rabbit(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_custom_local_context_nats(): +async def test_custom_local_context_nats() -> None: from docs.docs_src.getting_started.context.nats.custom_local_context import ( broker, handle, @@ -69,9 +69,9 @@ async def test_custom_local_context_nats(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_custom_local_context_redis(): +async def test_custom_local_context_redis() -> None: from docs.docs_src.getting_started.context.redis.custom_local_context import ( broker, handle, diff --git a/tests/a_docs/getting_started/context/test_default_arguments.py b/tests/docs/getting_started/context/test_default_arguments.py similarity index 83% rename from tests/a_docs/getting_started/context/test_default_arguments.py rename to tests/docs/getting_started/context/test_default_arguments.py index 0f0360bbb6..31acc73b2c 100644 --- a/tests/a_docs/getting_started/context/test_default_arguments.py +++ b/tests/docs/getting_started/context/test_default_arguments.py @@ -9,9 +9,9 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_default_arguments_kafka(): +async def test_default_arguments_kafka() -> None: from docs.docs_src.getting_started.context.kafka.default_arguments import ( broker, handle, @@ -24,9 +24,9 @@ async def test_default_arguments_kafka(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_default_arguments_confluent(): +async def test_default_arguments_confluent() -> None: from docs.docs_src.getting_started.context.confluent.default_arguments import ( broker, handle, @@ -39,9 +39,9 @@ async def test_default_arguments_confluent(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_default_arguments_rabbit(): +async def test_default_arguments_rabbit() -> None: from docs.docs_src.getting_started.context.rabbit.default_arguments import ( broker, handle, @@ -54,9 +54,9 @@ async def test_default_arguments_rabbit(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_default_arguments_nats(): +async def test_default_arguments_nats() -> None: from docs.docs_src.getting_started.context.nats.default_arguments import ( broker, handle, @@ -69,9 +69,9 @@ async def test_default_arguments_nats(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_default_arguments_redis(): +async def test_default_arguments_redis() -> None: from docs.docs_src.getting_started.context.redis.default_arguments import ( broker, handle, diff --git a/tests/a_docs/getting_started/context/test_existed_context.py b/tests/docs/getting_started/context/test_existed_context.py similarity index 80% rename from tests/a_docs/getting_started/context/test_existed_context.py rename to tests/docs/getting_started/context/test_existed_context.py index 22c14f4760..ad9211fda7 100644 --- a/tests/a_docs/getting_started/context/test_existed_context.py +++ b/tests/docs/getting_started/context/test_existed_context.py @@ -9,16 +9,16 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_existed_context_kafka(): +async def test_existed_context_kafka() -> None: from docs.docs_src.getting_started.context.kafka.existed_context import ( broker_object, ) from faststream.kafka import TestKafkaBroker @broker_object.subscriber("response") - async def resp(): ... + async def resp() -> None: ... async with TestKafkaBroker(broker_object) as br: await br.publish("Hi!", "test-topic") @@ -27,16 +27,16 @@ async def resp(): ... assert resp.mock.call_count == 2 -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_existed_context_confluent(): +async def test_existed_context_confluent() -> None: from docs.docs_src.getting_started.context.confluent.existed_context import ( broker_object, ) from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker @broker_object.subscriber("response") - async def resp(): ... + async def resp() -> None: ... async with TestConfluentKafkaBroker(broker_object) as br: await br.publish("Hi!", "test-topic") @@ -45,16 +45,16 @@ async def resp(): ... assert resp.mock.call_count == 2 -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_existed_context_rabbit(): +async def test_existed_context_rabbit() -> None: from docs.docs_src.getting_started.context.rabbit.existed_context import ( broker_object, ) from faststream.rabbit import TestRabbitBroker @broker_object.subscriber("response") - async def resp(): ... + async def resp() -> None: ... async with TestRabbitBroker(broker_object) as br: await br.publish("Hi!", "test-queue") @@ -63,16 +63,16 @@ async def resp(): ... assert resp.mock.call_count == 2 -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_existed_context_nats(): +async def test_existed_context_nats() -> None: from docs.docs_src.getting_started.context.nats.existed_context import ( broker_object, ) from faststream.nats import TestNatsBroker @broker_object.subscriber("response") - async def resp(): ... + async def resp() -> None: ... async with TestNatsBroker(broker_object) as br: await br.publish("Hi!", "test-subject") @@ -81,16 +81,16 @@ async def resp(): ... assert resp.mock.call_count == 2 -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_existed_context_redis(): +async def test_existed_context_redis() -> None: from docs.docs_src.getting_started.context.redis.existed_context import ( broker_object, ) from faststream.redis import TestRedisBroker @broker_object.subscriber("response") - async def resp(): ... + async def resp() -> None: ... async with TestRedisBroker(broker_object) as br: await br.publish("Hi!", "test-channel") diff --git a/tests/a_docs/getting_started/context/test_fields_access.py b/tests/docs/getting_started/context/test_fields_access.py similarity index 84% rename from tests/a_docs/getting_started/context/test_fields_access.py rename to tests/docs/getting_started/context/test_fields_access.py index 084ade7abb..10b6d07bd7 100644 --- a/tests/a_docs/getting_started/context/test_fields_access.py +++ b/tests/docs/getting_started/context/test_fields_access.py @@ -9,9 +9,9 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_fields_access_kafka(): +async def test_fields_access_kafka() -> None: from docs.docs_src.getting_started.context.kafka.fields_access import ( broker, handle, @@ -24,9 +24,9 @@ async def test_fields_access_kafka(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_fields_access_confluent(): +async def test_fields_access_confluent() -> None: from docs.docs_src.getting_started.context.confluent.fields_access import ( broker, handle, @@ -39,9 +39,9 @@ async def test_fields_access_confluent(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_fields_access_rabbit(): +async def test_fields_access_rabbit() -> None: from docs.docs_src.getting_started.context.rabbit.fields_access import ( broker, handle, @@ -54,9 +54,9 @@ async def test_fields_access_rabbit(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_fields_access_nats(): +async def test_fields_access_nats() -> None: from docs.docs_src.getting_started.context.nats.fields_access import ( broker, handle, @@ -69,9 +69,9 @@ async def test_fields_access_nats(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_fields_access_redis(): +async def test_fields_access_redis() -> None: from docs.docs_src.getting_started.context.redis.fields_access import ( broker, handle, diff --git a/tests/docs/getting_started/context/test_initial.py b/tests/docs/getting_started/context/test_initial.py new file mode 100644 index 0000000000..8acf57b037 --- /dev/null +++ b/tests/docs/getting_started/context/test_initial.py @@ -0,0 +1,79 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_kafka() -> None: + from docs.docs_src.getting_started.context.kafka.initial import broker + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker) as br: + await br.publish("", "test-topic") + await br.publish("", "test-topic") + + assert broker.context.get("collector") == ["", ""] + broker.context.clear() + + +@pytest.mark.asyncio() +@require_confluent +async def test_confluent() -> None: + from docs.docs_src.getting_started.context.confluent.initial import broker + from faststream.confluent import TestKafkaBroker + + async with TestKafkaBroker(broker) as br: + await br.publish("", "test-topic") + await br.publish("", "test-topic") + + assert broker.context.get("collector") == ["", ""] + broker.context.clear() + + +@pytest.mark.asyncio() +@require_aiopika +async def test_rabbit() -> None: + from docs.docs_src.getting_started.context.rabbit.initial import broker + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker) as br: + await br.publish("", "test-queue") + await br.publish("", "test-queue") + + assert broker.context.get("collector") == ["", ""] + broker.context.clear() + + +@pytest.mark.asyncio() +@require_nats +async def test_nats() -> None: + from docs.docs_src.getting_started.context.nats.initial import broker + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker) as br: + await br.publish("", "test-subject") + await br.publish("", "test-subject") + + assert broker.context.get("collector") == ["", ""] + broker.context.clear() + + +@pytest.mark.asyncio() +@require_redis +async def test_redis() -> None: + from docs.docs_src.getting_started.context.redis.initial import broker + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker) as br: + await br.publish("", "test-channel") + await br.publish("", "test-channel") + + assert broker.context.get("collector") == ["", ""] + broker.context.clear() diff --git a/tests/a_docs/getting_started/context/test_manual_local_context.py b/tests/docs/getting_started/context/test_manual_local_context.py similarity index 82% rename from tests/a_docs/getting_started/context/test_manual_local_context.py rename to tests/docs/getting_started/context/test_manual_local_context.py index f6c119132e..950ff32a60 100644 --- a/tests/a_docs/getting_started/context/test_manual_local_context.py +++ b/tests/docs/getting_started/context/test_manual_local_context.py @@ -9,9 +9,9 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_manual_local_context_kafka(): +async def test_manual_local_context_kafka() -> None: from docs.docs_src.getting_started.context.kafka.manual_local_context import ( broker, handle, @@ -24,9 +24,9 @@ async def test_manual_local_context_kafka(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_manual_local_context_confluent(): +async def test_manual_local_context_confluent() -> None: from docs.docs_src.getting_started.context.confluent.manual_local_context import ( broker, handle, @@ -39,9 +39,9 @@ async def test_manual_local_context_confluent(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_manual_local_context_rabbit(): +async def test_manual_local_context_rabbit() -> None: from docs.docs_src.getting_started.context.rabbit.manual_local_context import ( broker, handle, @@ -54,9 +54,9 @@ async def test_manual_local_context_rabbit(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_manual_local_context_nats(): +async def test_manual_local_context_nats() -> None: from docs.docs_src.getting_started.context.nats.manual_local_context import ( broker, handle, @@ -69,9 +69,9 @@ async def test_manual_local_context_nats(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_manual_local_context_redis(): +async def test_manual_local_context_redis() -> None: from docs.docs_src.getting_started.context.redis.manual_local_context import ( broker, handle, diff --git a/tests/a_docs/getting_started/context/test_nested.py b/tests/docs/getting_started/context/test_nested.py similarity index 87% rename from tests/a_docs/getting_started/context/test_nested.py rename to tests/docs/getting_started/context/test_nested.py index c782af41a1..070bc45e51 100644 --- a/tests/a_docs/getting_started/context/test_nested.py +++ b/tests/docs/getting_started/context/test_nested.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test(): +async def test() -> None: from docs.docs_src.getting_started.context.nested import broker, handler from faststream.rabbit import TestRabbitBroker diff --git a/tests/a_docs/getting_started/publishing/__init__.py b/tests/docs/getting_started/dependencies/__init__.py similarity index 100% rename from tests/a_docs/getting_started/publishing/__init__.py rename to tests/docs/getting_started/dependencies/__init__.py diff --git a/tests/a_docs/getting_started/routers/__init__.py b/tests/docs/getting_started/dependencies/basic/__init__.py similarity index 100% rename from tests/a_docs/getting_started/routers/__init__.py rename to tests/docs/getting_started/dependencies/basic/__init__.py diff --git a/tests/a_docs/getting_started/dependencies/basic/test_base.py b/tests/docs/getting_started/dependencies/basic/test_base.py similarity index 100% rename from tests/a_docs/getting_started/dependencies/basic/test_base.py rename to tests/docs/getting_started/dependencies/basic/test_base.py diff --git a/tests/docs/getting_started/dependencies/basic/test_depends.py b/tests/docs/getting_started/dependencies/basic/test_depends.py new file mode 100644 index 0000000000..4a54aa65a6 --- /dev/null +++ b/tests/docs/getting_started/dependencies/basic/test_depends.py @@ -0,0 +1,79 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_depends_kafka() -> None: + from docs.docs_src.getting_started.dependencies.basic.kafka.depends import ( + broker, + handler, + ) + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker): + await broker.publish({}, "test") + handler.mock.assert_called_once_with({}) + + +@pytest.mark.asyncio() +@require_confluent +async def test_depends_confluent() -> None: + from docs.docs_src.getting_started.dependencies.basic.confluent.depends import ( + broker, + handler, + ) + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker): + await broker.publish({}, "test") + handler.mock.assert_called_once_with({}) + + +@pytest.mark.asyncio() +@require_aiopika +async def test_depends_rabbit() -> None: + from docs.docs_src.getting_started.dependencies.basic.rabbit.depends import ( + broker, + handler, + ) + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker): + await broker.publish({}, "test") + handler.mock.assert_called_once_with({}) + + +@pytest.mark.asyncio() +@require_nats +async def test_depends_nats() -> None: + from docs.docs_src.getting_started.dependencies.basic.nats.depends import ( + broker, + handler, + ) + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker): + await broker.publish({}, "test") + handler.mock.assert_called_once_with({}) + + +@pytest.mark.asyncio() +@require_redis +async def test_depends_redis() -> None: + from docs.docs_src.getting_started.dependencies.basic.redis.depends import ( + broker, + handler, + ) + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker): + await broker.publish({}, "test") + handler.mock.assert_called_once_with({}) diff --git a/tests/a_docs/getting_started/dependencies/basic/test_nested_depends.py b/tests/docs/getting_started/dependencies/basic/test_nested_depends.py similarity index 83% rename from tests/a_docs/getting_started/dependencies/basic/test_nested_depends.py rename to tests/docs/getting_started/dependencies/basic/test_nested_depends.py index a09d1aa27d..4acdadb0d9 100644 --- a/tests/a_docs/getting_started/dependencies/basic/test_nested_depends.py +++ b/tests/docs/getting_started/dependencies/basic/test_nested_depends.py @@ -9,9 +9,9 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_nested_depends_kafka(): +async def test_nested_depends_kafka() -> None: from docs.docs_src.getting_started.dependencies.basic.kafka.nested_depends import ( broker, handler, @@ -23,9 +23,9 @@ async def test_nested_depends_kafka(): handler.mock.assert_called_once_with({}) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_nested_depends_confluent(): +async def test_nested_depends_confluent() -> None: from docs.docs_src.getting_started.dependencies.basic.confluent.nested_depends import ( broker, handler, @@ -37,9 +37,9 @@ async def test_nested_depends_confluent(): handler.mock.assert_called_once_with({}) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_nested_depends_rabbit(): +async def test_nested_depends_rabbit() -> None: from docs.docs_src.getting_started.dependencies.basic.rabbit.nested_depends import ( broker, handler, @@ -51,9 +51,9 @@ async def test_nested_depends_rabbit(): handler.mock.assert_called_once_with({}) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_nested_depends_nats(): +async def test_nested_depends_nats() -> None: from docs.docs_src.getting_started.dependencies.basic.nats.nested_depends import ( broker, handler, @@ -65,9 +65,9 @@ async def test_nested_depends_nats(): handler.mock.assert_called_once_with({}) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_nested_depends_redis(): +async def test_nested_depends_redis() -> None: from docs.docs_src.getting_started.dependencies.basic.redis.nested_depends import ( broker, handler, diff --git a/tests/docs/getting_started/dependencies/test_basic.py b/tests/docs/getting_started/dependencies/test_basic.py new file mode 100644 index 0000000000..98acd5658f --- /dev/null +++ b/tests/docs/getting_started/dependencies/test_basic.py @@ -0,0 +1,23 @@ +import pytest + +from faststream import TestApp +from tests.marks import require_aiokafka + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_basic_kafka() -> None: + from docs.docs_src.getting_started.dependencies.basic_kafka import ( + app, + broker, + handle, + ) + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker), TestApp(app): + handle.mock.assert_called_once_with( + { + "name": "John", + "user_id": 1, + }, + ) diff --git a/tests/a_docs/getting_started/dependencies/test_class.py b/tests/docs/getting_started/dependencies/test_class.py similarity index 85% rename from tests/a_docs/getting_started/dependencies/test_class.py rename to tests/docs/getting_started/dependencies/test_class.py index 5bbfd16850..74fe4d89e3 100644 --- a/tests/a_docs/getting_started/dependencies/test_class.py +++ b/tests/docs/getting_started/dependencies/test_class.py @@ -4,9 +4,9 @@ from tests.marks import require_aiokafka -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_basic_kafka(): +async def test_basic_kafka() -> None: from docs.docs_src.getting_started.dependencies.class_kafka import ( app, broker, @@ -19,5 +19,5 @@ async def test_basic_kafka(): { "name": "John", "user_id": 1, - } + }, ) diff --git a/tests/a_docs/getting_started/dependencies/test_global.py b/tests/docs/getting_started/dependencies/test_global.py similarity index 88% rename from tests/a_docs/getting_started/dependencies/test_global.py rename to tests/docs/getting_started/dependencies/test_global.py index 4d543aabb4..528ed0954b 100644 --- a/tests/a_docs/getting_started/dependencies/test_global.py +++ b/tests/docs/getting_started/dependencies/test_global.py @@ -4,9 +4,9 @@ from tests.marks import require_aiokafka -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_global_kafka(): +async def test_global_kafka() -> None: from docs.docs_src.getting_started.dependencies.global_kafka import ( app, broker, @@ -19,7 +19,7 @@ async def test_global_kafka(): { "name": "John", "user_id": 1, - } + }, ) with pytest.raises(ValueError): # noqa: PT011 diff --git a/tests/a_docs/getting_started/dependencies/test_global_broker.py b/tests/docs/getting_started/dependencies/test_global_broker.py similarity index 87% rename from tests/a_docs/getting_started/dependencies/test_global_broker.py rename to tests/docs/getting_started/dependencies/test_global_broker.py index c0b9de9295..5f033c8135 100644 --- a/tests/a_docs/getting_started/dependencies/test_global_broker.py +++ b/tests/docs/getting_started/dependencies/test_global_broker.py @@ -4,9 +4,9 @@ from tests.marks import require_aiokafka -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_global_broker_kafka(): +async def test_global_broker_kafka() -> None: from docs.docs_src.getting_started.dependencies.global_broker_kafka import ( app, broker, @@ -19,7 +19,7 @@ async def test_global_broker_kafka(): { "name": "John", "user_id": 1, - } + }, ) with pytest.raises(ValueError): # noqa: PT011 diff --git a/tests/a_docs/getting_started/dependencies/test_sub_dep.py b/tests/docs/getting_started/dependencies/test_sub_dep.py similarity index 88% rename from tests/a_docs/getting_started/dependencies/test_sub_dep.py rename to tests/docs/getting_started/dependencies/test_sub_dep.py index 832b0c853c..e83ad3d034 100644 --- a/tests/a_docs/getting_started/dependencies/test_sub_dep.py +++ b/tests/docs/getting_started/dependencies/test_sub_dep.py @@ -4,9 +4,9 @@ from tests.marks import require_aiokafka -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_sub_dep_kafka(): +async def test_sub_dep_kafka() -> None: from docs.docs_src.getting_started.dependencies.sub_dep_kafka import ( app, broker, @@ -19,7 +19,7 @@ async def test_sub_dep_kafka(): { "name": "John", "user_id": 1, - } + }, ) with pytest.raises(AssertionError): diff --git a/tests/a_docs/getting_started/dependencies/test_yield.py b/tests/docs/getting_started/dependencies/test_yield.py similarity index 86% rename from tests/a_docs/getting_started/dependencies/test_yield.py rename to tests/docs/getting_started/dependencies/test_yield.py index 7ad8615a3f..3079c2d98b 100644 --- a/tests/a_docs/getting_started/dependencies/test_yield.py +++ b/tests/docs/getting_started/dependencies/test_yield.py @@ -4,9 +4,9 @@ from tests.marks import require_aiokafka -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_yield_kafka(): +async def test_yield_kafka() -> None: from docs.docs_src.getting_started.dependencies.yield_kafka import ( app, broker, diff --git a/tests/a_docs/getting_started/serialization/__init__.py b/tests/docs/getting_started/index/__init__.py similarity index 100% rename from tests/a_docs/getting_started/serialization/__init__.py rename to tests/docs/getting_started/index/__init__.py diff --git a/tests/docs/getting_started/index/test_basic.py b/tests/docs/getting_started/index/test_basic.py new file mode 100644 index 0000000000..5abf32818d --- /dev/null +++ b/tests/docs/getting_started/index/test_basic.py @@ -0,0 +1,69 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_quickstart_index_kafka() -> None: + from docs.docs_src.getting_started.index.base_kafka import base_handler, broker + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker) as br: + await br.publish("", "test") + + base_handler.mock.assert_called_once_with("") + + +@pytest.mark.asyncio() +@require_confluent +async def test_quickstart_index_confluent() -> None: + from docs.docs_src.getting_started.index.base_confluent import base_handler, broker + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker) as br: + await br.publish("", "test") + + base_handler.mock.assert_called_once_with("") + + +@pytest.mark.asyncio() +@require_aiopika +async def test_quickstart_index_rabbit() -> None: + from docs.docs_src.getting_started.index.base_rabbit import base_handler, broker + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker) as br: + await br.publish("", "test") + + base_handler.mock.assert_called_once_with("") + + +@pytest.mark.asyncio() +@require_nats +async def test_quickstart_index_nats() -> None: + from docs.docs_src.getting_started.index.base_nats import base_handler, broker + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker) as br: + await br.publish("", "test") + + base_handler.mock.assert_called_once_with("") + + +@pytest.mark.asyncio() +@require_redis +async def test_quickstart_index_redis() -> None: + from docs.docs_src.getting_started.index.base_redis import base_handler, broker + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker) as br: + await br.publish("", "test") + + base_handler.mock.assert_called_once_with("") diff --git a/tests/a_docs/getting_started/subscription/__init__.py b/tests/docs/getting_started/lifespan/__init__.py similarity index 100% rename from tests/a_docs/getting_started/subscription/__init__.py rename to tests/docs/getting_started/lifespan/__init__.py diff --git a/tests/docs/getting_started/lifespan/test_ml.py b/tests/docs/getting_started/lifespan/test_ml.py new file mode 100644 index 0000000000..7c205172a6 --- /dev/null +++ b/tests/docs/getting_started/lifespan/test_ml.py @@ -0,0 +1,70 @@ +import pytest + +from faststream import TestApp +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiopika +async def test_rabbit_ml_lifespan() -> None: + from docs.docs_src.getting_started.lifespan.rabbit.ml import app, broker, predict + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker), TestApp(app): + assert await (await broker.request(1.0, "test")).decode() == {"result": 42.0} + + predict.mock.assert_called_once_with(1.0) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_kafka_ml_lifespan() -> None: + from docs.docs_src.getting_started.lifespan.kafka.ml import app, broker, predict + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker), TestApp(app): + assert await (await broker.request(1.0, "test")).decode() == {"result": 42.0} + + predict.mock.assert_called_once_with(1.0) + + +@pytest.mark.asyncio() +@require_confluent +async def test_confluent_ml_lifespan() -> None: + from docs.docs_src.getting_started.lifespan.confluent.ml import app, broker, predict + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker), TestApp(app): + assert await (await broker.request(1.0, "test")).decode() == {"result": 42.0} + + predict.mock.assert_called_once_with(1.0) + + +@pytest.mark.asyncio() +@require_nats +async def test_nats_ml_lifespan() -> None: + from docs.docs_src.getting_started.lifespan.nats.ml import app, broker, predict + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker), TestApp(app): + assert await (await broker.request(1.0, "test")).decode() == {"result": 42.0} + + predict.mock.assert_called_once_with(1.0) + + +@pytest.mark.asyncio() +@require_redis +async def test_redis_ml_lifespan() -> None: + from docs.docs_src.getting_started.lifespan.redis.ml import app, broker, predict + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker), TestApp(app): + assert await (await broker.request(1.0, "test")).decode() == {"result": 42.0} + + predict.mock.assert_called_once_with(1.0) diff --git a/tests/docs/getting_started/lifespan/test_ml_context.py b/tests/docs/getting_started/lifespan/test_ml_context.py new file mode 100644 index 0000000000..4765957d61 --- /dev/null +++ b/tests/docs/getting_started/lifespan/test_ml_context.py @@ -0,0 +1,90 @@ +import pytest + +from faststream import TestApp +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiopika +async def test_rabbit_ml_lifespan() -> None: + from docs.docs_src.getting_started.lifespan.rabbit.ml_context import ( + app, + broker, + predict, + ) + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker), TestApp(app): + assert await (await broker.request(1.0, "test")).decode() == {"result": 42.0} + + predict.mock.assert_called_once_with(1.0) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_kafka_ml_lifespan() -> None: + from docs.docs_src.getting_started.lifespan.kafka.ml_context import ( + app, + broker, + predict, + ) + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker), TestApp(app): + assert await (await broker.request(1.0, "test")).decode() == {"result": 42.0} + + predict.mock.assert_called_once_with(1.0) + + +@pytest.mark.asyncio() +@require_confluent +async def test_confluent_ml_lifespan() -> None: + from docs.docs_src.getting_started.lifespan.confluent.ml_context import ( + app, + broker, + predict, + ) + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker), TestApp(app): + assert await (await broker.request(1.0, "test")).decode() == {"result": 42.0} + + predict.mock.assert_called_once_with(1.0) + + +@pytest.mark.asyncio() +@require_nats +async def test_nats_ml_lifespan() -> None: + from docs.docs_src.getting_started.lifespan.nats.ml_context import ( + app, + broker, + predict, + ) + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker), TestApp(app): + assert await (await broker.request(1.0, "test")).decode() == {"result": 42.0} + + predict.mock.assert_called_once_with(1.0) + + +@pytest.mark.asyncio() +@require_redis +async def test_redis_ml_lifespan() -> None: + from docs.docs_src.getting_started.lifespan.redis.ml_context import ( + app, + broker, + predict, + ) + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker), TestApp(app): + assert await (await broker.request(1.0, "test")).decode() == {"result": 42.0} + + predict.mock.assert_called_once_with(1.0) diff --git a/tests/docs/getting_started/lifespan/test_multi.py b/tests/docs/getting_started/lifespan/test_multi.py new file mode 100644 index 0000000000..8d4b0e2a98 --- /dev/null +++ b/tests/docs/getting_started/lifespan/test_multi.py @@ -0,0 +1,11 @@ +import pytest + +from faststream import TestApp + + +@pytest.mark.asyncio() +async def test_multi_lifespan() -> None: + from docs.docs_src.getting_started.lifespan.multiple import app + + async with TestApp(app): + assert app.context.get("field") == 1 diff --git a/tests/docs/getting_started/lifespan/test_testing.py b/tests/docs/getting_started/lifespan/test_testing.py new file mode 100644 index 0000000000..66dbb522b5 --- /dev/null +++ b/tests/docs/getting_started/lifespan/test_testing.py @@ -0,0 +1,59 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_redis +async def test_lifespan_redis() -> None: + from docs.docs_src.getting_started.lifespan.redis.testing import ( + test_lifespan as _test_lifespan_red, + ) + + await _test_lifespan_red() + + +@pytest.mark.asyncio() +@require_confluent +async def test_lifespan_confluent() -> None: + from docs.docs_src.getting_started.lifespan.confluent.testing import ( + test_lifespan as _test_lifespan_confluent, + ) + + await _test_lifespan_confluent() + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_lifespan_kafka() -> None: + from docs.docs_src.getting_started.lifespan.kafka.testing import ( + test_lifespan as _test_lifespan_k, + ) + + await _test_lifespan_k() + + +@pytest.mark.asyncio() +@require_aiopika +async def test_lifespan_rabbit() -> None: + from docs.docs_src.getting_started.lifespan.rabbit.testing import ( + test_lifespan as _test_lifespan_r, + ) + + await _test_lifespan_r() + + +@pytest.mark.asyncio() +@require_nats +async def test_lifespan_nats() -> None: + from docs.docs_src.getting_started.lifespan.nats.testing import ( + test_lifespan as _test_lifespan_n, + ) + + await _test_lifespan_n() diff --git a/tests/a_docs/index/__init__.py b/tests/docs/getting_started/publishing/__init__.py similarity index 100% rename from tests/a_docs/index/__init__.py rename to tests/docs/getting_started/publishing/__init__.py diff --git a/tests/docs/getting_started/publishing/test_broker.py b/tests/docs/getting_started/publishing/test_broker.py new file mode 100644 index 0000000000..e60b37bd19 --- /dev/null +++ b/tests/docs/getting_started/publishing/test_broker.py @@ -0,0 +1,90 @@ +import pytest + +from faststream import TestApp +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_broker_kafka() -> None: + from docs.docs_src.getting_started.publishing.kafka.broker import ( + app, + broker, + handle, + handle_next, + ) + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker), TestApp(app): + handle.mock.assert_called_once_with("") + handle_next.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_confluent +async def test_broker_confluent() -> None: + from docs.docs_src.getting_started.publishing.confluent.broker import ( + app, + broker, + handle, + handle_next, + ) + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker), TestApp(app): + handle.mock.assert_called_once_with("") + handle_next.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_aiopika +async def test_broker_rabbit() -> None: + from docs.docs_src.getting_started.publishing.rabbit.broker import ( + app, + broker, + handle, + handle_next, + ) + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker), TestApp(app): + handle.mock.assert_called_once_with("") + handle_next.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_nats +async def test_broker_nats() -> None: + from docs.docs_src.getting_started.publishing.nats.broker import ( + app, + broker, + handle, + handle_next, + ) + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker), TestApp(app): + handle.mock.assert_called_once_with("") + handle_next.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_redis +async def test_broker_redis() -> None: + from docs.docs_src.getting_started.publishing.redis.broker import ( + app, + broker, + handle, + handle_next, + ) + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker), TestApp(app): + handle.mock.assert_called_once_with("") + handle_next.mock.assert_called_once_with("Hi!") diff --git a/tests/a_docs/getting_started/publishing/test_broker_context.py b/tests/docs/getting_started/publishing/test_broker_context.py similarity index 81% rename from tests/a_docs/getting_started/publishing/test_broker_context.py rename to tests/docs/getting_started/publishing/test_broker_context.py index 9893ef819c..0128b2bbc5 100644 --- a/tests/a_docs/getting_started/publishing/test_broker_context.py +++ b/tests/docs/getting_started/publishing/test_broker_context.py @@ -10,10 +10,10 @@ ) -@pytest.mark.asyncio -@pytest.mark.kafka +@pytest.mark.asyncio() +@pytest.mark.kafka() @require_aiokafka -async def test_broker_context_kafka(): +async def test_broker_context_kafka() -> None: from docs.docs_src.getting_started.publishing.kafka.broker_context import ( app, broker, @@ -26,10 +26,10 @@ async def test_broker_context_kafka(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio -@pytest.mark.confluent +@pytest.mark.asyncio() +@pytest.mark.confluent() @require_confluent -async def test_broker_context_confluent(): +async def test_broker_context_confluent() -> None: from docs.docs_src.getting_started.publishing.confluent.broker_context import ( app, broker, @@ -42,10 +42,10 @@ async def test_broker_context_confluent(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio -@pytest.mark.nats +@pytest.mark.asyncio() +@pytest.mark.nats() @require_nats -async def test_broker_context_nats(): +async def test_broker_context_nats() -> None: from docs.docs_src.getting_started.publishing.nats.broker_context import ( app, broker, @@ -58,10 +58,10 @@ async def test_broker_context_nats(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio -@pytest.mark.rabbit +@pytest.mark.asyncio() +@pytest.mark.rabbit() @require_aiopika -async def test_broker_context_rabbit(): +async def test_broker_context_rabbit() -> None: from docs.docs_src.getting_started.publishing.rabbit.broker_context import ( app, broker, @@ -74,10 +74,10 @@ async def test_broker_context_rabbit(): handle.mock.assert_called_once_with("Hi!") -@pytest.mark.asyncio -@pytest.mark.redis +@pytest.mark.asyncio() +@pytest.mark.redis() @require_redis -async def test_broker_context_redis(): +async def test_broker_context_redis() -> None: from docs.docs_src.getting_started.publishing.redis.broker_context import ( app, broker, diff --git a/tests/docs/getting_started/publishing/test_decorator.py b/tests/docs/getting_started/publishing/test_decorator.py new file mode 100644 index 0000000000..1ded7fdf57 --- /dev/null +++ b/tests/docs/getting_started/publishing/test_decorator.py @@ -0,0 +1,95 @@ +import pytest + +from faststream import TestApp +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_decorator_kafka() -> None: + from docs.docs_src.getting_started.publishing.kafka.decorator import ( + app, + broker, + handle, + handle_next, + ) + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker), TestApp(app): + handle.mock.assert_called_once_with("") + handle_next.mock.assert_called_once_with("Hi!") + next(iter(broker._publishers)).mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_confluent +async def test_decorator_confluent() -> None: + from docs.docs_src.getting_started.publishing.confluent.decorator import ( + app, + broker, + handle, + handle_next, + ) + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker), TestApp(app): + handle.mock.assert_called_once_with("") + handle_next.mock.assert_called_once_with("Hi!") + next(iter(broker._publishers)).mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_aiopika +async def test_decorator_rabbit() -> None: + from docs.docs_src.getting_started.publishing.rabbit.decorator import ( + app, + broker, + handle, + handle_next, + ) + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker), TestApp(app): + handle.mock.assert_called_once_with("") + handle_next.mock.assert_called_once_with("Hi!") + next(iter(broker._publishers)).mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_nats +async def test_decorator_nats() -> None: + from docs.docs_src.getting_started.publishing.nats.decorator import ( + app, + broker, + handle, + handle_next, + ) + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker), TestApp(app): + handle.mock.assert_called_once_with("") + handle_next.mock.assert_called_once_with("Hi!") + next(iter(broker._publishers)).mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_redis +async def test_decorator_redis() -> None: + from docs.docs_src.getting_started.publishing.redis.decorator import ( + app, + broker, + handle, + handle_next, + ) + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker), TestApp(app): + handle.mock.assert_called_once_with("") + handle_next.mock.assert_called_once_with("Hi!") + next(iter(broker._publishers)).mock.assert_called_once_with("Hi!") diff --git a/tests/docs/getting_started/publishing/test_direct.py b/tests/docs/getting_started/publishing/test_direct.py new file mode 100644 index 0000000000..de6030506a --- /dev/null +++ b/tests/docs/getting_started/publishing/test_direct.py @@ -0,0 +1,59 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_handle_kafka() -> None: + from docs.docs_src.getting_started.publishing.kafka.direct_testing import ( + test_handle as test_handle_k, + ) + + await test_handle_k() + + +@pytest.mark.asyncio() +@require_confluent +async def test_handle_confluent() -> None: + from docs.docs_src.getting_started.publishing.confluent.direct_testing import ( + test_handle as test_handle_confluent, + ) + + await test_handle_confluent() + + +@pytest.mark.asyncio() +@require_aiopika +async def test_handle_rabbit() -> None: + from docs.docs_src.getting_started.publishing.rabbit.direct_testing import ( + test_handle as test_handle_r, + ) + + await test_handle_r() + + +@pytest.mark.asyncio() +@require_nats +async def test_handle_nats() -> None: + from docs.docs_src.getting_started.publishing.nats.direct_testing import ( + test_handle as test_handle_n, + ) + + await test_handle_n() + + +@pytest.mark.asyncio() +@require_redis +async def test_handle_redis() -> None: + from docs.docs_src.getting_started.publishing.redis.direct_testing import ( + test_handle as test_handle_red, + ) + + await test_handle_red() diff --git a/tests/docs/getting_started/publishing/test_object.py b/tests/docs/getting_started/publishing/test_object.py new file mode 100644 index 0000000000..0b26e89ab3 --- /dev/null +++ b/tests/docs/getting_started/publishing/test_object.py @@ -0,0 +1,59 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_handle_kafka() -> None: + from docs.docs_src.getting_started.publishing.kafka.object_testing import ( + test_handle as test_handle_k, + ) + + await test_handle_k() + + +@pytest.mark.asyncio() +@require_confluent +async def test_handle_confluent() -> None: + from docs.docs_src.getting_started.publishing.confluent.object_testing import ( + test_handle as test_handle_confluent, + ) + + await test_handle_confluent() + + +@pytest.mark.asyncio() +@require_aiopika +async def test_handle_rabbit() -> None: + from docs.docs_src.getting_started.publishing.rabbit.object_testing import ( + test_handle as test_handle_r, + ) + + await test_handle_r() + + +@pytest.mark.asyncio() +@require_nats +async def test_handle_nats() -> None: + from docs.docs_src.getting_started.publishing.nats.object_testing import ( + test_handle as test_handle_n, + ) + + await test_handle_n() + + +@pytest.mark.asyncio() +@require_redis +async def test_handle_redis() -> None: + from docs.docs_src.getting_started.publishing.redis.object_testing import ( + test_handle as test_handle_red, + ) + + await test_handle_red() diff --git a/tests/a_docs/integration/__init__.py b/tests/docs/getting_started/routers/__init__.py similarity index 100% rename from tests/a_docs/integration/__init__.py rename to tests/docs/getting_started/routers/__init__.py diff --git a/tests/docs/getting_started/routers/test_base.py b/tests/docs/getting_started/routers/test_base.py new file mode 100644 index 0000000000..2422d0319b --- /dev/null +++ b/tests/docs/getting_started/routers/test_base.py @@ -0,0 +1,90 @@ +import pytest + +from faststream import TestApp +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_base_router_kafka() -> None: + from docs.docs_src.getting_started.routers.kafka.router import ( + app, + broker, + handle, + handle_response, + ) + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker), TestApp(app): + handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) + handle_response.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_confluent +async def test_base_router_confluent() -> None: + from docs.docs_src.getting_started.routers.confluent.router import ( + app, + broker, + handle, + handle_response, + ) + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker), TestApp(app): + handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) + handle_response.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_aiopika +async def test_base_router_rabbit() -> None: + from docs.docs_src.getting_started.routers.rabbit.router import ( + app, + broker, + handle, + handle_response, + ) + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker), TestApp(app): + handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) + handle_response.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_nats +async def test_base_router_nats() -> None: + from docs.docs_src.getting_started.routers.nats.router import ( + app, + broker, + handle, + handle_response, + ) + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker), TestApp(app): + handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) + handle_response.mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_redis +async def test_base_router_redis() -> None: + from docs.docs_src.getting_started.routers.redis.router import ( + app, + broker, + handle, + handle_response, + ) + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker), TestApp(app): + handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) + handle_response.mock.assert_called_once_with("Hi!") diff --git a/tests/docs/getting_started/routers/test_delay.py b/tests/docs/getting_started/routers/test_delay.py new file mode 100644 index 0000000000..2828f8892a --- /dev/null +++ b/tests/docs/getting_started/routers/test_delay.py @@ -0,0 +1,95 @@ +import pytest + +from faststream import TestApp +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_delay_router_kafka() -> None: + from docs.docs_src.getting_started.routers.kafka.router_delay import ( + app, + broker, + ) + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker) as br, TestApp(app): + br.subscribers[1].calls[0].handler.mock.assert_called_once_with( + {"name": "John", "user_id": 1}, + ) + + br.publishers[0].mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_confluent +async def test_delay_router_confluent() -> None: + from docs.docs_src.getting_started.routers.confluent.router_delay import ( + app, + broker, + ) + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker) as br, TestApp(app): + br.subscribers[1].calls[0].handler.mock.assert_called_once_with( + {"name": "John", "user_id": 1}, + ) + + br.publishers[0].mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_aiopika +async def test_delay_router_rabbit() -> None: + from docs.docs_src.getting_started.routers.rabbit.router_delay import ( + app, + broker, + ) + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker) as br, TestApp(app): + br.subscribers[1].calls[0].handler.mock.assert_called_once_with( + {"name": "John", "user_id": 1}, + ) + + br.publishers[0].mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_nats +async def test_delay_router_nats() -> None: + from docs.docs_src.getting_started.routers.nats.router_delay import ( + app, + broker, + ) + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker) as br, TestApp(app): + br.subscribers[1].calls[0].handler.mock.assert_called_once_with( + {"name": "John", "user_id": 1}, + ) + + br.publishers[0].mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_redis +async def test_delay_router_redis() -> None: + from docs.docs_src.getting_started.routers.redis.router_delay import ( + app, + broker, + ) + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker) as br, TestApp(app): + br.subscribers[1].calls[0].handler.mock.assert_called_once_with( + {"name": "John", "user_id": 1}, + ) + + br.publishers[0].mock.assert_called_once_with("Hi!") diff --git a/tests/docs/getting_started/routers/test_delay_equal.py b/tests/docs/getting_started/routers/test_delay_equal.py new file mode 100644 index 0000000000..35de53f804 --- /dev/null +++ b/tests/docs/getting_started/routers/test_delay_equal.py @@ -0,0 +1,125 @@ +import pytest + +from faststream import TestApp +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_delay_router_kafka() -> None: + from docs.docs_src.getting_started.routers.kafka.delay_equal import ( + app, + broker, + ) + from docs.docs_src.getting_started.routers.kafka.router_delay import ( + broker as control_broker, + ) + from faststream.kafka import TestKafkaBroker + + assert len(broker.subscribers) == len(control_broker.subscribers) + assert len(broker.publishers) == len(control_broker.publishers) + + async with TestKafkaBroker(broker) as br, TestApp(app): + br.subscribers[1].calls[0].handler.mock.assert_called_once_with( + {"name": "John", "user_id": 1}, + ) + + br.publishers[0].mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_confluent +async def test_delay_router_confluent() -> None: + from docs.docs_src.getting_started.routers.confluent.delay_equal import ( + app, + broker, + ) + from docs.docs_src.getting_started.routers.confluent.router_delay import ( + broker as control_broker, + ) + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + assert len(broker.subscribers) == len(control_broker.subscribers) + assert len(broker.publishers) == len(control_broker.publishers) + + async with TestConfluentKafkaBroker(broker) as br, TestApp(app): + br.subscribers[1].calls[0].handler.mock.assert_called_once_with( + {"name": "John", "user_id": 1}, + ) + + br.publishers[0].mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_aiopika +async def test_delay_router_rabbit() -> None: + from docs.docs_src.getting_started.routers.rabbit.delay_equal import ( + app, + broker, + ) + from docs.docs_src.getting_started.routers.rabbit.router_delay import ( + broker as control_broker, + ) + from faststream.rabbit import TestRabbitBroker + + assert len(broker.subscribers) == len(control_broker.subscribers) + assert len(broker.publishers) == len(control_broker.publishers) + + async with TestRabbitBroker(broker) as br, TestApp(app): + br.subscribers[1].calls[0].handler.mock.assert_called_once_with( + {"name": "John", "user_id": 1}, + ) + + br.publishers[0].mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_nats +async def test_delay_router_nats() -> None: + from docs.docs_src.getting_started.routers.nats.delay_equal import ( + app, + broker, + ) + from docs.docs_src.getting_started.routers.nats.router_delay import ( + broker as control_broker, + ) + from faststream.nats import TestNatsBroker + + assert len(broker.subscribers) == len(control_broker.subscribers) + assert len(broker.publishers) == len(control_broker.publishers) + + async with TestNatsBroker(broker) as br, TestApp(app): + br.subscribers[1].calls[0].handler.mock.assert_called_once_with( + {"name": "John", "user_id": 1}, + ) + + br.publishers[0].mock.assert_called_once_with("Hi!") + + +@pytest.mark.asyncio() +@require_redis +async def test_delay_router_redis() -> None: + from docs.docs_src.getting_started.routers.redis.delay_equal import ( + app, + broker, + ) + from docs.docs_src.getting_started.routers.redis.router_delay import ( + broker as control_broker, + ) + from faststream.redis import TestRedisBroker + + assert len(broker.subscribers) == len(control_broker.subscribers) + assert len(broker.publishers) == len(control_broker.publishers) + + async with TestRedisBroker(broker) as br, TestApp(app): + br.subscribers[1].calls[0].handler.mock.assert_called_once_with( + {"name": "John", "user_id": 1}, + ) + + br.publishers[0].mock.assert_called_once_with("Hi!") diff --git a/tests/a_docs/integration/fastapi/__init__.py b/tests/docs/getting_started/serialization/__init__.py similarity index 100% rename from tests/a_docs/integration/fastapi/__init__.py rename to tests/docs/getting_started/serialization/__init__.py diff --git a/tests/a_docs/getting_started/serialization/test_parser.py b/tests/docs/getting_started/serialization/test_parser.py similarity index 84% rename from tests/a_docs/getting_started/serialization/test_parser.py rename to tests/docs/getting_started/serialization/test_parser.py index 3a68f8abf0..73a909c722 100644 --- a/tests/a_docs/getting_started/serialization/test_parser.py +++ b/tests/docs/getting_started/serialization/test_parser.py @@ -10,9 +10,9 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_parser_nats(): +async def test_parser_nats() -> None: from docs.docs_src.getting_started.serialization.parser_nats import ( app, broker, @@ -24,9 +24,9 @@ async def test_parser_nats(): handle.mock.assert_called_once_with("") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_parser_kafka(): +async def test_parser_kafka() -> None: from docs.docs_src.getting_started.serialization.parser_kafka import ( app, broker, @@ -38,9 +38,9 @@ async def test_parser_kafka(): handle.mock.assert_called_once_with("") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_parser_confluent(): +async def test_parser_confluent() -> None: from docs.docs_src.getting_started.serialization.parser_confluent import ( app, broker, @@ -52,9 +52,9 @@ async def test_parser_confluent(): handle.mock.assert_called_once_with("") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_parser_rabbit(): +async def test_parser_rabbit() -> None: from docs.docs_src.getting_started.serialization.parser_rabbit import ( app, broker, @@ -66,9 +66,9 @@ async def test_parser_rabbit(): handle.mock.assert_called_once_with("") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_parser_redis(): +async def test_parser_redis() -> None: from docs.docs_src.getting_started.serialization.parser_redis import ( app, broker, diff --git a/tests/a_docs/integration/http/__init__.py b/tests/docs/getting_started/subscription/__init__.py similarity index 100% rename from tests/a_docs/integration/http/__init__.py rename to tests/docs/getting_started/subscription/__init__.py diff --git a/tests/docs/getting_started/subscription/test_annotated.py b/tests/docs/getting_started/subscription/test_annotated.py new file mode 100644 index 0000000000..d3d9759238 --- /dev/null +++ b/tests/docs/getting_started/subscription/test_annotated.py @@ -0,0 +1,107 @@ +from typing import Any, TypeAlias + +import pytest +from fast_depends.exceptions import ValidationError + +from faststream._internal.broker import BrokerUsecase +from faststream._internal.endpoint.subscriber import SubscriberUsecase +from faststream._internal.testing.broker import TestBroker +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + +Setup: TypeAlias = tuple[ + BrokerUsecase[Any, Any], + SubscriberUsecase[Any], + type[TestBroker], +] + + +@pytest.mark.asyncio() +class BaseCase: + async def test_handle(self, setup: Setup) -> None: + broker, handle, test_class = setup + + async with test_class(broker) as br: + await br.publish({"name": "John", "user_id": 1}, "test") + handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) + + assert handle.mock is None + + async def test_validation_error(self, setup: Setup) -> None: + broker, handle, test_class = setup + + async with test_class(broker) as br: + with pytest.raises(ValidationError): + await br.publish("wrong message", "test") + + handle.mock.assert_called_once_with("wrong message") + + +@require_aiokafka +class TestKafka(BaseCase): + @pytest.fixture(scope="class") + def setup(self) -> Setup: + from docs.docs_src.getting_started.subscription.kafka.pydantic_annotated_fields import ( + broker, + handle, + ) + from faststream.kafka import TestKafkaBroker + + return (broker, handle, TestKafkaBroker) + + +@require_confluent +class TestConfluent(BaseCase): + @pytest.fixture(scope="class") + def setup(self) -> Setup: + from docs.docs_src.getting_started.subscription.confluent.pydantic_annotated_fields import ( + broker, + handle, + ) + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + return (broker, handle, TestConfluentKafkaBroker) + + +@require_aiopika +class TestRabbit(BaseCase): + @pytest.fixture(scope="class") + def setup(self) -> Setup: + from docs.docs_src.getting_started.subscription.rabbit.pydantic_annotated_fields import ( + broker, + handle, + ) + from faststream.rabbit import TestRabbitBroker + + return (broker, handle, TestRabbitBroker) + + +@require_nats +class TestNats(BaseCase): + @pytest.fixture(scope="class") + def setup(self) -> Setup: + from docs.docs_src.getting_started.subscription.nats.pydantic_annotated_fields import ( + broker, + handle, + ) + from faststream.nats import TestNatsBroker + + return (broker, handle, TestNatsBroker) + + +@require_redis +class TestRedis(BaseCase): + @pytest.fixture(scope="class") + def setup(self) -> Setup: + from docs.docs_src.getting_started.subscription.redis.pydantic_annotated_fields import ( + broker, + handle, + ) + from faststream.redis import TestRedisBroker + + return (broker, handle, TestRedisBroker) diff --git a/tests/a_docs/getting_started/subscription/test_filter.py b/tests/docs/getting_started/subscription/test_filter.py similarity index 87% rename from tests/a_docs/getting_started/subscription/test_filter.py rename to tests/docs/getting_started/subscription/test_filter.py index 4789c6dda8..60264d60cc 100644 --- a/tests/a_docs/getting_started/subscription/test_filter.py +++ b/tests/docs/getting_started/subscription/test_filter.py @@ -10,9 +10,9 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_kafka_filtering(): +async def test_kafka_filtering() -> None: from docs.docs_src.getting_started.subscription.kafka.filter import ( app, broker, @@ -26,9 +26,9 @@ async def test_kafka_filtering(): default_handler.mock.assert_called_once_with("Hello, FastStream!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_confluent_filtering(): +async def test_confluent_filtering() -> None: from docs.docs_src.getting_started.subscription.confluent.filter import ( app, broker, @@ -42,9 +42,9 @@ async def test_confluent_filtering(): default_handler.mock.assert_called_once_with("Hello, FastStream!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_rabbit_filtering(): +async def test_rabbit_filtering() -> None: from docs.docs_src.getting_started.subscription.rabbit.filter import ( app, broker, @@ -58,9 +58,9 @@ async def test_rabbit_filtering(): default_handler.mock.assert_called_once_with("Hello, FastStream!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_nats_filtering(): +async def test_nats_filtering() -> None: from docs.docs_src.getting_started.subscription.nats.filter import ( app, broker, @@ -74,9 +74,9 @@ async def test_nats_filtering(): default_handler.mock.assert_called_once_with("Hello, FastStream!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_redis_filtering(): +async def test_redis_filtering() -> None: from docs.docs_src.getting_started.subscription.redis.filter import ( app, broker, diff --git a/tests/docs/getting_started/subscription/test_pydantic.py b/tests/docs/getting_started/subscription/test_pydantic.py new file mode 100644 index 0000000000..cef46891c3 --- /dev/null +++ b/tests/docs/getting_started/subscription/test_pydantic.py @@ -0,0 +1,79 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiopika +async def test_pydantic_model_rabbit() -> None: + from docs.docs_src.getting_started.subscription.rabbit.pydantic_model import ( + broker, + handle, + ) + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker) as br: + await br.publish({"name": "John", "user_id": 1}, "test-queue") + handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_pydantic_model_kafka() -> None: + from docs.docs_src.getting_started.subscription.kafka.pydantic_model import ( + broker, + handle, + ) + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker) as br: + await br.publish({"name": "John", "user_id": 1}, "test-topic") + handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) + + +@pytest.mark.asyncio() +@require_confluent +async def test_pydantic_model_confluent() -> None: + from docs.docs_src.getting_started.subscription.confluent.pydantic_model import ( + broker, + handle, + ) + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker) as br: + await br.publish({"name": "John", "user_id": 1}, "test-topic") + handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) + + +@pytest.mark.asyncio() +@require_nats +async def test_pydantic_model_nats() -> None: + from docs.docs_src.getting_started.subscription.nats.pydantic_model import ( + broker, + handle, + ) + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker) as br: + await br.publish({"name": "John", "user_id": 1}, "test-subject") + handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) + + +@pytest.mark.asyncio() +@require_redis +async def test_pydantic_model_redis() -> None: + from docs.docs_src.getting_started.subscription.redis.pydantic_model import ( + broker, + handle, + ) + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker) as br: + await br.publish({"name": "John", "user_id": 1}, "test-channel") + handle.mock.assert_called_once_with({"name": "John", "user_id": 1}) diff --git a/tests/docs/getting_started/subscription/test_real.py b/tests/docs/getting_started/subscription/test_real.py new file mode 100644 index 0000000000..f033a0a11e --- /dev/null +++ b/tests/docs/getting_started/subscription/test_real.py @@ -0,0 +1,119 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.kafka() +@pytest.mark.asyncio() +@require_aiokafka +async def test_handle_kafka() -> None: + from docs.docs_src.getting_started.subscription.kafka.real_testing import ( + test_handle as test_handle_k, + ) + + await test_handle_k() + + +@pytest.mark.kafka() +@pytest.mark.asyncio() +@require_aiokafka +async def test_validate_kafka() -> None: + from docs.docs_src.getting_started.subscription.kafka.real_testing import ( + test_validation_error as test_validation_error_k, + ) + + await test_validation_error_k() + + +@pytest.mark.confluent() +@pytest.mark.asyncio() +@require_confluent +async def test_handle_confluent() -> None: + from docs.docs_src.getting_started.subscription.confluent.real_testing import ( + test_handle as test_handle_confluent, + ) + + await test_handle_confluent() + + +@pytest.mark.asyncio() +@pytest.mark.confluent() +@require_confluent +async def test_validate_confluent() -> None: + from docs.docs_src.getting_started.subscription.confluent.real_testing import ( + test_validation_error as test_validation_error_confluent, + ) + + await test_validation_error_confluent() + + +@pytest.mark.asyncio() +@pytest.mark.rabbit() +@require_aiopika +async def test_handle_rabbit() -> None: + from docs.docs_src.getting_started.subscription.rabbit.real_testing import ( + test_handle as test_handle_r, + ) + + await test_handle_r() + + +@pytest.mark.asyncio() +@pytest.mark.rabbit() +@require_aiopika +async def test_validate_rabbit() -> None: + from docs.docs_src.getting_started.subscription.rabbit.real_testing import ( + test_validation_error as test_validation_error_r, + ) + + await test_validation_error_r() + + +@pytest.mark.asyncio() +@pytest.mark.nats() +@require_nats +async def test_handle_nats() -> None: + from docs.docs_src.getting_started.subscription.nats.real_testing import ( + test_handle as test_handle_n, + ) + + await test_handle_n() + + +@pytest.mark.asyncio() +@pytest.mark.nats() +@require_nats +async def test_validate_nats() -> None: + from docs.docs_src.getting_started.subscription.nats.real_testing import ( + test_validation_error as test_validation_error_n, + ) + + await test_validation_error_n() + + +@pytest.mark.asyncio() +@pytest.mark.redis() +@require_redis +async def test_handle_redis() -> None: + from docs.docs_src.getting_started.subscription.redis.real_testing import ( + test_handle as test_handle_red, + ) + + await test_handle_red() + + +@pytest.mark.asyncio() +@pytest.mark.redis() +@require_redis +async def test_validate_redis() -> None: + from docs.docs_src.getting_started.subscription.redis.real_testing import ( + test_validation_error as test_validation_error_red, + ) + + await test_validation_error_red() diff --git a/tests/docs/getting_started/subscription/test_testing.py b/tests/docs/getting_started/subscription/test_testing.py new file mode 100644 index 0000000000..1c1a786b21 --- /dev/null +++ b/tests/docs/getting_started/subscription/test_testing.py @@ -0,0 +1,119 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.kafka() +@pytest.mark.asyncio() +@require_aiokafka +async def test_handle_kafka() -> None: + from docs.docs_src.getting_started.subscription.kafka.testing import ( + test_handle as test_handle_k, + ) + + await test_handle_k() + + +@pytest.mark.kafka() +@pytest.mark.asyncio() +@require_aiokafka +async def test_validate_kafka() -> None: + from docs.docs_src.getting_started.subscription.kafka.testing import ( + test_validation_error as test_validation_error_k, + ) + + await test_validation_error_k() + + +@pytest.mark.confluent() +@pytest.mark.asyncio() +@require_confluent +async def test_handle_confluent() -> None: + from docs.docs_src.getting_started.subscription.confluent.testing import ( + test_handle as test_handle_confluent, + ) + + await test_handle_confluent() + + +@pytest.mark.asyncio() +@pytest.mark.confluent() +@require_confluent +async def test_validate_confluent() -> None: + from docs.docs_src.getting_started.subscription.confluent.testing import ( + test_validation_error as test_validation_error_confluent, + ) + + await test_validation_error_confluent() + + +@pytest.mark.asyncio() +@pytest.mark.rabbit() +@require_aiopika +async def test_handle_rabbit() -> None: + from docs.docs_src.getting_started.subscription.rabbit.testing import ( + test_handle as test_handle_r, + ) + + await test_handle_r() + + +@pytest.mark.asyncio() +@pytest.mark.rabbit() +@require_aiopika +async def test_validate_rabbit() -> None: + from docs.docs_src.getting_started.subscription.rabbit.testing import ( + test_validation_error as test_validation_error_r, + ) + + await test_validation_error_r() + + +@pytest.mark.asyncio() +@pytest.mark.nats() +@require_nats +async def test_handle_nats() -> None: + from docs.docs_src.getting_started.subscription.nats.testing import ( + test_handle as test_handle_n, + ) + + await test_handle_n() + + +@pytest.mark.asyncio() +@pytest.mark.nats() +@require_nats +async def test_validate_nats() -> None: + from docs.docs_src.getting_started.subscription.nats.testing import ( + test_validation_error as test_validation_error_n, + ) + + await test_validation_error_n() + + +@pytest.mark.asyncio() +@pytest.mark.redis() +@require_redis +async def test_handle_redis() -> None: + from docs.docs_src.getting_started.subscription.redis.testing import ( + test_handle as test_handle_rd, + ) + + await test_handle_rd() + + +@pytest.mark.asyncio() +@pytest.mark.redis() +@require_redis +async def test_validate_redis() -> None: + from docs.docs_src.getting_started.subscription.redis.testing import ( + test_validation_error as test_validation_error_rd, + ) + + await test_validation_error_rd() diff --git a/tests/a_docs/kafka/ack/__init__.py b/tests/docs/index/__init__.py similarity index 100% rename from tests/a_docs/kafka/ack/__init__.py rename to tests/docs/index/__init__.py diff --git a/tests/docs/index/test_basic.py b/tests/docs/index/test_basic.py new file mode 100644 index 0000000000..a518aff1ff --- /dev/null +++ b/tests/docs/index/test_basic.py @@ -0,0 +1,89 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_index_kafka_base() -> None: + from docs.docs_src.index.kafka.basic import broker, handle_msg + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker) as br: + await br.publish({"user": "John", "user_id": 1}, "in-topic") + + handle_msg.mock.assert_called_once_with({"user": "John", "user_id": 1}) + + list(br._publishers)[0].mock.assert_called_once_with( # noqa: RUF015 + "User: 1 - John registered", + ) + + +@pytest.mark.asyncio() +@require_confluent +async def test_index_confluent_base() -> None: + from docs.docs_src.index.confluent.basic import broker, handle_msg + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(broker) as br: + await br.publish({"user": "John", "user_id": 1}, "in-topic") + + handle_msg.mock.assert_called_once_with({"user": "John", "user_id": 1}) + + list(br._publishers)[0].mock.assert_called_once_with( # noqa: RUF015 + "User: 1 - John registered", + ) + + +@pytest.mark.asyncio() +@require_aiopika +async def test_index_rabbit_base() -> None: + from docs.docs_src.index.rabbit.basic import broker, handle_msg + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(broker) as br: + await br.publish({"user": "John", "user_id": 1}, "in-queue") + + handle_msg.mock.assert_called_once_with({"user": "John", "user_id": 1}) + + list(br._publishers)[0].mock.assert_called_once_with( # noqa: RUF015 + "User: 1 - John registered", + ) + + +@pytest.mark.asyncio() +@require_nats +async def test_index_nats_base() -> None: + from docs.docs_src.index.nats.basic import broker, handle_msg + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(broker) as br: + await br.publish({"user": "John", "user_id": 1}, "in-subject") + + handle_msg.mock.assert_called_once_with({"user": "John", "user_id": 1}) + + list(br._publishers)[0].mock.assert_called_once_with( # noqa: RUF015 + "User: 1 - John registered", + ) + + +@pytest.mark.asyncio() +@require_redis +async def test_index_redis_base() -> None: + from docs.docs_src.index.redis.basic import broker, handle_msg + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(broker) as br: + await br.publish({"user": "John", "user_id": 1}, "in-channel") + + handle_msg.mock.assert_called_once_with({"user": "John", "user_id": 1}) + + list(br._publishers)[0].mock.assert_called_once_with( # noqa: RUF015 + "User: 1 - John registered", + ) diff --git a/tests/a_docs/index/test_dependencies.py b/tests/docs/index/test_dependencies.py similarity index 87% rename from tests/a_docs/index/test_dependencies.py rename to tests/docs/index/test_dependencies.py index c0ba757a81..8d8eedad25 100644 --- a/tests/a_docs/index/test_dependencies.py +++ b/tests/docs/index/test_dependencies.py @@ -3,9 +3,9 @@ from tests.marks import require_aiokafka -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_index_dep(): +async def test_index_dep() -> None: from docs.docs_src.index.dependencies import base_handler, broker from faststream.kafka import TestKafkaBroker diff --git a/tests/docs/index/test_pydantic.py b/tests/docs/index/test_pydantic.py new file mode 100644 index 0000000000..2267d15675 --- /dev/null +++ b/tests/docs/index/test_pydantic.py @@ -0,0 +1,93 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_kafka_correct() -> None: + from docs.docs_src.index.kafka.test import test_correct as test_k_correct + + await test_k_correct() + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_kafka_invalid() -> None: + from docs.docs_src.index.kafka.test import test_invalid as test_k_invalid + + await test_k_invalid() + + +@pytest.mark.asyncio() +@require_confluent +async def test_confluent_correct() -> None: + from docs.docs_src.index.confluent.test import ( + test_correct as test_confluent_correct, + ) + + await test_confluent_correct() + + +@pytest.mark.asyncio() +@require_confluent +async def test_confluent_invalid() -> None: + from docs.docs_src.index.confluent.test import ( + test_invalid as test_confluent_invalid, + ) + + await test_confluent_invalid() + + +@pytest.mark.asyncio() +@require_aiopika +async def test_rabbit_correct() -> None: + from docs.docs_src.index.rabbit.test import test_correct as test_r_correct + + await test_r_correct() + + +@pytest.mark.asyncio() +@require_aiopika +async def test_rabbit_invalid() -> None: + from docs.docs_src.index.rabbit.test import test_invalid as test_r_invalid + + await test_r_invalid() + + +@pytest.mark.asyncio() +@require_nats +async def test_nats_correct() -> None: + from docs.docs_src.index.nats.test import test_correct as test_n_correct + + await test_n_correct() + + +@pytest.mark.asyncio() +@require_nats +async def test_nats_invalid() -> None: + from docs.docs_src.index.nats.test import test_invalid as test_n_invalid + + await test_n_invalid() + + +@pytest.mark.asyncio() +@require_redis +async def test_redis_correct() -> None: + from docs.docs_src.index.redis.test import test_correct as test_red_correct + + await test_red_correct() + + +@pytest.mark.asyncio() +@require_redis +async def test_redis_invalid() -> None: + from docs.docs_src.index.redis.test import test_invalid as test_red_invalid + + await test_red_invalid() diff --git a/tests/a_docs/kafka/basic/__init__.py b/tests/docs/integration/__init__.py similarity index 100% rename from tests/a_docs/kafka/basic/__init__.py rename to tests/docs/integration/__init__.py diff --git a/tests/a_docs/kafka/batch_consuming_pydantic/__init__.py b/tests/docs/integration/fastapi/__init__.py similarity index 100% rename from tests/a_docs/kafka/batch_consuming_pydantic/__init__.py rename to tests/docs/integration/fastapi/__init__.py diff --git a/tests/docs/integration/fastapi/test_base.py b/tests/docs/integration/fastapi/test_base.py new file mode 100644 index 0000000000..223df2514d --- /dev/null +++ b/tests/docs/integration/fastapi/test_base.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_fastapi_kafka_base() -> None: + from docs.docs_src.integrations.fastapi.kafka.base import app, hello, router + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(router.broker) as br: + with TestClient(app) as client: + assert client.get("/").text == '"Hello, HTTP!"' + + await br.publish({"m": {}}, "test") + + hello.mock.assert_called_once_with({"m": {}}) + + list(br._publishers)[0].mock.assert_called_with( # noqa: RUF015 + {"response": "Hello, Kafka!"}, + ) + + +@pytest.mark.asyncio() +@require_confluent +async def test_fastapi_confluent_base() -> None: + from docs.docs_src.integrations.fastapi.confluent.base import app, hello, router + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + async with TestConfluentKafkaBroker(router.broker) as br: + with TestClient(app) as client: + assert client.get("/").text == '"Hello, HTTP!"' + + await br.publish({"m": {}}, "test") + + hello.mock.assert_called_once_with({"m": {}}) + + list(br._publishers)[0].mock.assert_called_with( # noqa: RUF015 + {"response": "Hello, Kafka!"}, + ) + + +@pytest.mark.asyncio() +@require_aiopika +async def test_fastapi_rabbit_base() -> None: + from docs.docs_src.integrations.fastapi.rabbit.base import app, hello, router + from faststream.rabbit import TestRabbitBroker + + async with TestRabbitBroker(router.broker) as br: + with TestClient(app) as client: + assert client.get("/").text == '"Hello, HTTP!"' + + await br.publish({"m": {}}, "test") + + hello.mock.assert_called_once_with({"m": {}}) + + list(br._publishers)[0].mock.assert_called_with( # noqa: RUF015 + {"response": "Hello, Rabbit!"}, + ) + + +@pytest.mark.asyncio() +@require_nats +async def test_fastapi_nats_base() -> None: + from docs.docs_src.integrations.fastapi.nats.base import app, hello, router + from faststream.nats import TestNatsBroker + + async with TestNatsBroker(router.broker) as br: + with TestClient(app) as client: + assert client.get("/").text == '"Hello, HTTP!"' + + await br.publish({"m": {}}, "test") + + hello.mock.assert_called_once_with({"m": {}}) + + list(br._publishers)[0].mock.assert_called_with( # noqa: RUF015 + {"response": "Hello, NATS!"}, + ) + + +@pytest.mark.asyncio() +@require_redis +async def test_fastapi_redis_base() -> None: + from docs.docs_src.integrations.fastapi.redis.base import app, hello, router + from faststream.redis import TestRedisBroker + + async with TestRedisBroker(router.broker) as br: + with TestClient(app) as client: + assert client.get("/").text == '"Hello, HTTP!"' + + await br.publish({"m": {}}, "test") + + hello.mock.assert_called_once_with({"m": {}}) + + list(br._publishers)[0].mock.assert_called_with( # noqa: RUF015 + {"response": "Hello, Redis!"}, + ) diff --git a/tests/docs/integration/fastapi/test_depends.py b/tests/docs/integration/fastapi/test_depends.py new file mode 100644 index 0000000000..7361b3ad5f --- /dev/null +++ b/tests/docs/integration/fastapi/test_depends.py @@ -0,0 +1,90 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_fastapi_kafka_depends() -> None: + from docs.docs_src.integrations.fastapi.kafka.depends import app, router + from faststream.kafka import TestKafkaBroker + + @router.subscriber("test") + async def handler() -> None: ... + + async with TestKafkaBroker(router.broker): + with TestClient(app) as client: + assert client.get("/").text == '"Hello, HTTP!"' + + handler.mock.assert_called_once_with("Hello, Kafka!") + + +@pytest.mark.asyncio() +@require_confluent +async def test_fastapi_confluent_depends() -> None: + from docs.docs_src.integrations.fastapi.confluent.depends import app, router + from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker + + @router.subscriber("test") + async def handler() -> None: ... + + async with TestConfluentKafkaBroker(router.broker): + with TestClient(app) as client: + assert client.get("/").text == '"Hello, HTTP!"' + + handler.mock.assert_called_once_with("Hello, Kafka!") + + +@pytest.mark.asyncio() +@require_aiopika +async def test_fastapi_rabbit_depends() -> None: + from docs.docs_src.integrations.fastapi.rabbit.depends import app, router + from faststream.rabbit import TestRabbitBroker + + @router.subscriber("test") + async def handler() -> None: ... + + async with TestRabbitBroker(router.broker): + with TestClient(app) as client: + assert client.get("/").text == '"Hello, HTTP!"' + + handler.mock.assert_called_once_with("Hello, Rabbit!") + + +@pytest.mark.asyncio() +@require_nats +async def test_fastapi_nats_depends() -> None: + from docs.docs_src.integrations.fastapi.nats.depends import app, router + from faststream.nats import TestNatsBroker + + @router.subscriber("test") + async def handler() -> None: ... + + async with TestNatsBroker(router.broker): + with TestClient(app) as client: + assert client.get("/").text == '"Hello, HTTP!"' + + handler.mock.assert_called_once_with("Hello, NATS!") + + +@pytest.mark.asyncio() +@require_redis +async def test_fastapi_redis_depends() -> None: + from docs.docs_src.integrations.fastapi.redis.depends import app, router + from faststream.redis import TestRedisBroker + + @router.subscriber("test") + async def handler() -> None: ... + + async with TestRedisBroker(router.broker): + with TestClient(app) as client: + assert client.get("/").text == '"Hello, HTTP!"' + + handler.mock.assert_called_once_with("Hello, Redis!") diff --git a/tests/a_docs/integration/fastapi/test_routers.py b/tests/docs/integration/fastapi/test_routers.py similarity index 89% rename from tests/a_docs/integration/fastapi/test_routers.py rename to tests/docs/integration/fastapi/test_routers.py index c8122b0414..376ac7a304 100644 --- a/tests/a_docs/integration/fastapi/test_routers.py +++ b/tests/docs/integration/fastapi/test_routers.py @@ -11,10 +11,10 @@ class BaseCase: - def test_running(self, data): + def test_running(self, data) -> None: app, broker = data - handlers = broker._subscribers.values() + handlers = broker.subscribers assert len(handlers) == 2 for h in handlers: @@ -25,7 +25,7 @@ def test_running(self, data): assert h.running -@pytest.mark.kafka +@pytest.mark.kafka() @require_aiokafka class TestKafka(BaseCase): @pytest.fixture(scope="class") @@ -35,7 +35,7 @@ def data(self): return (app, core_router.broker) -@pytest.mark.confluent +@pytest.mark.confluent() @require_confluent class TestConfluent(BaseCase): @pytest.fixture(scope="class") @@ -48,7 +48,7 @@ def data(self): return (app, core_router.broker) -@pytest.mark.nats +@pytest.mark.nats() @require_nats class TestNats(BaseCase): @pytest.fixture(scope="class") @@ -58,7 +58,7 @@ def data(self): return (app, core_router.broker) -@pytest.mark.rabbit +@pytest.mark.rabbit() @require_aiopika class TestRabbit(BaseCase): @pytest.fixture(scope="class") @@ -68,7 +68,7 @@ def data(self): return (app, core_router.broker) -@pytest.mark.redis +@pytest.mark.redis() @require_redis class TestRedis(BaseCase): @pytest.fixture(scope="class") diff --git a/tests/a_docs/integration/fastapi/test_send.py b/tests/docs/integration/fastapi/test_send.py similarity index 80% rename from tests/a_docs/integration/fastapi/test_send.py rename to tests/docs/integration/fastapi/test_send.py index 13bc7376ef..a0ca67ee64 100644 --- a/tests/a_docs/integration/fastapi/test_send.py +++ b/tests/docs/integration/fastapi/test_send.py @@ -10,14 +10,14 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_fastapi_kafka_send(): +async def test_fastapi_kafka_send() -> None: from docs.docs_src.integrations.fastapi.kafka.send import app, router from faststream.kafka import TestKafkaBroker @router.subscriber("test") - async def handler(): ... + async def handler() -> None: ... async with TestKafkaBroker(router.broker): with TestClient(app) as client: @@ -26,14 +26,14 @@ async def handler(): ... handler.mock.assert_called_once_with("Hello, Kafka!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_fastapi_confluent_send(): +async def test_fastapi_confluent_send() -> None: from docs.docs_src.integrations.fastapi.confluent.send import app, router from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker @router.subscriber("test") - async def handler(): ... + async def handler() -> None: ... async with TestConfluentKafkaBroker(router.broker): with TestClient(app) as client: @@ -42,14 +42,14 @@ async def handler(): ... handler.mock.assert_called_once_with("Hello, Kafka!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_fastapi_rabbit_send(): +async def test_fastapi_rabbit_send() -> None: from docs.docs_src.integrations.fastapi.rabbit.send import app, router from faststream.rabbit import TestRabbitBroker @router.subscriber("test") - async def handler(): ... + async def handler() -> None: ... async with TestRabbitBroker(router.broker): with TestClient(app) as client: @@ -58,14 +58,14 @@ async def handler(): ... handler.mock.assert_called_once_with("Hello, Rabbit!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_fastapi_nats_send(): +async def test_fastapi_nats_send() -> None: from docs.docs_src.integrations.fastapi.nats.send import app, router from faststream.nats import TestNatsBroker @router.subscriber("test") - async def handler(): ... + async def handler() -> None: ... async with TestNatsBroker(router.broker): with TestClient(app) as client: @@ -74,14 +74,14 @@ async def handler(): ... handler.mock.assert_called_once_with("Hello, NATS!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_fastapi_redis_send(): +async def test_fastapi_redis_send() -> None: from docs.docs_src.integrations.fastapi.redis.send import app, router from faststream.redis import TestRedisBroker @router.subscriber("test") - async def handler(): ... + async def handler() -> None: ... async with TestRedisBroker(router.broker): with TestClient(app) as client: diff --git a/tests/a_docs/integration/fastapi/test_startup.py b/tests/docs/integration/fastapi/test_startup.py similarity index 77% rename from tests/a_docs/integration/fastapi/test_startup.py rename to tests/docs/integration/fastapi/test_startup.py index 011cea69fc..80e62afbeb 100644 --- a/tests/a_docs/integration/fastapi/test_startup.py +++ b/tests/docs/integration/fastapi/test_startup.py @@ -10,70 +10,70 @@ ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_fastapi_kafka_startup(): +async def test_fastapi_kafka_startup() -> None: from docs.docs_src.integrations.fastapi.kafka.startup import app, hello, router from faststream.kafka import TestKafkaBroker @router.subscriber("test") - async def handler(): ... + async def handler() -> None: ... async with TestKafkaBroker(router.broker): with TestClient(app): hello.mock.assert_called_once_with("Hello!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_confluent -async def test_fastapi_confluent_startup(): +async def test_fastapi_confluent_startup() -> None: from docs.docs_src.integrations.fastapi.confluent.startup import app, hello, router from faststream.confluent import TestKafkaBroker as TestConfluentKafkaBroker @router.subscriber("test") - async def handler(): ... + async def handler() -> None: ... async with TestConfluentKafkaBroker(router.broker): with TestClient(app): hello.mock.assert_called_once_with("Hello!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_fastapi_rabbit_startup(): +async def test_fastapi_rabbit_startup() -> None: from docs.docs_src.integrations.fastapi.rabbit.startup import app, hello, router from faststream.rabbit import TestRabbitBroker @router.subscriber("test") - async def handler(): ... + async def handler() -> None: ... async with TestRabbitBroker(router.broker): with TestClient(app): hello.mock.assert_called_once_with("Hello!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_fastapi_nats_startup(): +async def test_fastapi_nats_startup() -> None: from docs.docs_src.integrations.fastapi.nats.startup import app, hello, router from faststream.nats import TestNatsBroker @router.subscriber("test") - async def handler(): ... + async def handler() -> None: ... async with TestNatsBroker(router.broker): with TestClient(app): hello.mock.assert_called_once_with("Hello!") -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_fastapi_redis_startup(): +async def test_fastapi_redis_startup() -> None: from docs.docs_src.integrations.fastapi.redis.startup import app, hello, router from faststream.redis import TestRedisBroker @router.subscriber("test") - async def handler(): ... + async def handler() -> None: ... async with TestRedisBroker(router.broker): with TestClient(app): diff --git a/tests/docs/integration/fastapi/test_test.py b/tests/docs/integration/fastapi/test_test.py new file mode 100644 index 0000000000..55e682b079 --- /dev/null +++ b/tests/docs/integration/fastapi/test_test.py @@ -0,0 +1,49 @@ +import pytest + +from tests.marks import ( + require_aiokafka, + require_aiopika, + require_confluent, + require_nats, + require_redis, +) + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_kafka() -> None: + from docs.docs_src.integrations.fastapi.kafka.test import test_router + + await test_router() + + +@pytest.mark.asyncio() +@require_confluent +async def test_confluent() -> None: + from docs.docs_src.integrations.fastapi.confluent.test import test_router + + await test_router() + + +@pytest.mark.asyncio() +@require_aiopika +async def test_rabbit() -> None: + from docs.docs_src.integrations.fastapi.rabbit.test import test_router + + await test_router() + + +@pytest.mark.asyncio() +@require_nats +async def test_nats() -> None: + from docs.docs_src.integrations.fastapi.nats.test import test_router + + await test_router() + + +@pytest.mark.asyncio() +@require_redis +async def test_redis() -> None: + from docs.docs_src.integrations.fastapi.redis.test import test_router + + await test_router() diff --git a/tests/a_docs/kafka/consumes_basics/__init__.py b/tests/docs/integration/http/__init__.py similarity index 100% rename from tests/a_docs/kafka/consumes_basics/__init__.py rename to tests/docs/integration/http/__init__.py diff --git a/tests/docs/integration/http/test_fastapi.py b/tests/docs/integration/http/test_fastapi.py new file mode 100644 index 0000000000..3e51eb1401 --- /dev/null +++ b/tests/docs/integration/http/test_fastapi.py @@ -0,0 +1,23 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.marks import require_aiokafka + + +@pytest.mark.asyncio() +@require_aiokafka +async def test_fastapi_raw_integration() -> None: + from docs.docs_src.integrations.http_frameworks_integrations.fastapi import ( + app, + base_handler, + broker, + ) + from faststream.kafka import TestKafkaBroker + + async with TestKafkaBroker(broker): + with TestClient(app) as client: + assert client.get("/").json() == {"Hello": "World"} + + await broker.publish("", "test") + + base_handler.mock.assert_called_once_with("") diff --git a/tests/docs/kafka/__init__.py b/tests/docs/kafka/__init__.py new file mode 100644 index 0000000000..bd6bc708fc --- /dev/null +++ b/tests/docs/kafka/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("aiokafka") diff --git a/tests/a_docs/kafka/publish_batch/__init__.py b/tests/docs/kafka/ack/__init__.py similarity index 100% rename from tests/a_docs/kafka/publish_batch/__init__.py rename to tests/docs/kafka/ack/__init__.py diff --git a/tests/docs/kafka/ack/test_errors.py b/tests/docs/kafka/ack/test_errors.py new file mode 100644 index 0000000000..d150c2bf0d --- /dev/null +++ b/tests/docs/kafka/ack/test_errors.py @@ -0,0 +1,24 @@ +from unittest.mock import patch + +import pytest +from aiokafka import AIOKafkaConsumer + +from faststream.kafka import TestApp, TestKafkaBroker +from tests.tools import spy_decorator + + +@pytest.mark.asyncio() +@pytest.mark.kafka() +@pytest.mark.slow() +async def test_ack_exc() -> None: + from docs.docs_src.kafka.ack.errors import app, broker, handle + + with patch.object( + AIOKafkaConsumer, + "commit", + spy_decorator(AIOKafkaConsumer.commit), + ) as m: + async with TestKafkaBroker(broker, with_real=True), TestApp(app): + await handle.wait_call(10) + + assert m.mock.call_count diff --git a/tests/a_docs/kafka/publish_example/__init__.py b/tests/docs/kafka/basic/__init__.py similarity index 100% rename from tests/a_docs/kafka/publish_example/__init__.py rename to tests/docs/kafka/basic/__init__.py diff --git a/tests/docs/kafka/basic/test_basic.py b/tests/docs/kafka/basic/test_basic.py new file mode 100644 index 0000000000..7121749a4e --- /dev/null +++ b/tests/docs/kafka/basic/test_basic.py @@ -0,0 +1,15 @@ +import pytest + +from faststream.kafka import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_basic() -> None: + from docs.docs_src.kafka.basic.basic import broker, on_input_data + + publisher = list(broker._publishers)[0] # noqa: RUF015 + + async with TestKafkaBroker(broker) as br: + await br.publish({"data": 1.0}, "input_data") + on_input_data.mock.assert_called_once_with({"data": 1.0}) + publisher.mock.assert_called_once_with({"data": 2.0}) diff --git a/tests/docs/kafka/basic/test_cmd_run.py b/tests/docs/kafka/basic/test_cmd_run.py new file mode 100644 index 0000000000..45c9f89191 --- /dev/null +++ b/tests/docs/kafka/basic/test_cmd_run.py @@ -0,0 +1,35 @@ +import traceback +from typing import Any +from unittest.mock import MagicMock + +import pytest +from typer.testing import CliRunner + +from faststream._internal.cli.main import cli +from faststream.app import FastStream + + +@pytest.mark.kafka() +def test_run_cmd( + runner: CliRunner, + mock: MagicMock, + monkeypatch: pytest.MonkeyPatch, + kafka_basic_project: str, +) -> None: + async def patched_run(self: FastStream, *args: Any, **kwargs: Any) -> None: + await self.start() + await self.stop() + mock() + + with monkeypatch.context() as m: + m.setattr(FastStream, "run", patched_run) + r = runner.invoke( + cli, + [ + "run", + kafka_basic_project, + ], + ) + + assert r.exit_code == 0, (r.output, traceback.format_exception(r.exception)) + mock.assert_called_once() diff --git a/tests/a_docs/kafka/publish_with_partition_key/__init__.py b/tests/docs/kafka/batch_consuming_pydantic/__init__.py similarity index 100% rename from tests/a_docs/kafka/publish_with_partition_key/__init__.py rename to tests/docs/kafka/batch_consuming_pydantic/__init__.py diff --git a/tests/docs/kafka/batch_consuming_pydantic/test_app.py b/tests/docs/kafka/batch_consuming_pydantic/test_app.py new file mode 100644 index 0000000000..f080f6d94a --- /dev/null +++ b/tests/docs/kafka/batch_consuming_pydantic/test_app.py @@ -0,0 +1,21 @@ +import pytest + +from docs.docs_src.kafka.batch_consuming_pydantic.app import ( + HelloWorld, + broker, + handle_batch, +) +from faststream.kafka import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_me() -> None: + async with TestKafkaBroker(broker): + await broker.publish_batch( + HelloWorld(msg="First Hello"), + HelloWorld(msg="Second Hello"), + topic="test_batch", + ) + handle_batch.mock.assert_called_with( + [dict(HelloWorld(msg="First Hello")), dict(HelloWorld(msg="Second Hello"))], + ) diff --git a/tests/a_docs/kafka/publisher_object/__init__.py b/tests/docs/kafka/consumes_basics/__init__.py similarity index 100% rename from tests/a_docs/kafka/publisher_object/__init__.py rename to tests/docs/kafka/consumes_basics/__init__.py diff --git a/tests/docs/kafka/consumes_basics/test_app.py b/tests/docs/kafka/consumes_basics/test_app.py new file mode 100644 index 0000000000..6a258153dd --- /dev/null +++ b/tests/docs/kafka/consumes_basics/test_app.py @@ -0,0 +1,15 @@ +import pytest + +from docs.docs_src.kafka.consumes_basics.app import ( + HelloWorld, + broker, + on_hello_world, +) +from faststream.kafka import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_base_app() -> None: + async with TestKafkaBroker(broker): + await broker.publish(HelloWorld(msg="First Hello"), "hello_world") + on_hello_world.mock.assert_called_with(dict(HelloWorld(msg="First Hello"))) diff --git a/tests/a_docs/kafka/raw_publish/__init__.py b/tests/docs/kafka/publish_batch/__init__.py similarity index 100% rename from tests/a_docs/kafka/raw_publish/__init__.py rename to tests/docs/kafka/publish_batch/__init__.py diff --git a/tests/docs/kafka/publish_batch/test_app.py b/tests/docs/kafka/publish_batch/test_app.py new file mode 100644 index 0000000000..d9fea8cfda --- /dev/null +++ b/tests/docs/kafka/publish_batch/test_app.py @@ -0,0 +1,32 @@ +import pytest + +from docs.docs_src.kafka.publish_batch.app import ( + Data, + broker, + decrease_and_increase, + on_input_data_1, + on_input_data_2, +) +from faststream.kafka import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_batch_publish_decorator() -> None: + async with TestKafkaBroker(broker): + await broker.publish(Data(data=2.0), "input_data_1") + + on_input_data_1.mock.assert_called_once_with(dict(Data(data=2.0))) + decrease_and_increase.mock.assert_called_once_with( + [dict(Data(data=1.0)), dict(Data(data=4.0))], + ) + + +@pytest.mark.asyncio() +async def test_batch_publish_call() -> None: + async with TestKafkaBroker(broker): + await broker.publish(Data(data=2.0), "input_data_2") + + on_input_data_2.mock.assert_called_once_with(dict(Data(data=2.0))) + decrease_and_increase.mock.assert_called_once_with( + [dict(Data(data=1.0)), dict(Data(data=4.0))], + ) diff --git a/tests/docs/kafka/publish_batch/test_issues.py b/tests/docs/kafka/publish_batch/test_issues.py new file mode 100644 index 0000000000..031d12929e --- /dev/null +++ b/tests/docs/kafka/publish_batch/test_issues.py @@ -0,0 +1,22 @@ +import pytest + +from faststream import FastStream +from faststream.kafka import KafkaBroker, TestKafkaBroker + +broker = KafkaBroker() +batch_producer = broker.publisher("response", batch=True) + + +@batch_producer +@broker.subscriber("test") +async def handle(msg: str) -> list[int]: + return [1, 2, 3] + + +app = FastStream(broker) + + +@pytest.mark.asyncio() +async def test_base_app() -> None: + async with TestKafkaBroker(broker): + await broker.publish("", "test") diff --git a/tests/a_docs/nats/ack/__init__.py b/tests/docs/kafka/publish_example/__init__.py similarity index 100% rename from tests/a_docs/nats/ack/__init__.py rename to tests/docs/kafka/publish_example/__init__.py diff --git a/tests/docs/kafka/publish_example/test_app.py b/tests/docs/kafka/publish_example/test_app.py new file mode 100644 index 0000000000..e9ab2a2038 --- /dev/null +++ b/tests/docs/kafka/publish_example/test_app.py @@ -0,0 +1,18 @@ +import pytest + +from docs.docs_src.kafka.publish_example.app import ( + Data, + broker, + on_input_data, + to_output_data, +) +from faststream.kafka import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_base_app() -> None: + async with TestKafkaBroker(broker): + await broker.publish(Data(data=0.2), "input_data") + + on_input_data.mock.assert_called_once_with(dict(Data(data=0.2))) + to_output_data.mock.assert_called_once_with(dict(Data(data=1.2))) diff --git a/tests/a_docs/nats/js/__init__.py b/tests/docs/kafka/publish_with_partition_key/__init__.py similarity index 100% rename from tests/a_docs/nats/js/__init__.py rename to tests/docs/kafka/publish_with_partition_key/__init__.py diff --git a/tests/docs/kafka/publish_with_partition_key/test_app.py b/tests/docs/kafka/publish_with_partition_key/test_app.py new file mode 100644 index 0000000000..5dd2cac875 --- /dev/null +++ b/tests/docs/kafka/publish_with_partition_key/test_app.py @@ -0,0 +1,30 @@ +import pytest + +from docs.docs_src.kafka.publish_with_partition_key.app import ( + Data, + broker, + on_input_data, + to_output_data, +) +from faststream.kafka import TestKafkaBroker + + +@pytest.mark.asyncio() +async def test_app() -> None: + async with TestKafkaBroker(broker): + await broker.publish(Data(data=0.2), "input_data", key=b"my_key") + + on_input_data.mock.assert_called_once_with(dict(Data(data=0.2))) + to_output_data.mock.assert_called_once_with(dict(Data(data=1.2))) + + +@pytest.mark.skip("we are not checking the key") +@pytest.mark.asyncio() +async def test_keys() -> None: + async with TestKafkaBroker(broker): + # we should be able to publish a message with the key + await broker.publish(Data(data=0.2), "input_data", key=b"my_key") + + # we need to check the key as well + on_input_data.mock.assert_called_once_with(dict(Data(data=0.2)), key=b"my_key") + to_output_data.mock.assert_called_once_with(dict(Data(data=1.2)), key=b"key") diff --git a/tests/a_docs/rabbit/ack/__init__.py b/tests/docs/kafka/publisher_object/__init__.py similarity index 100% rename from tests/a_docs/rabbit/ack/__init__.py rename to tests/docs/kafka/publisher_object/__init__.py diff --git a/tests/a_docs/kafka/publisher_object/test_publisher_object.py b/tests/docs/kafka/publisher_object/test_publisher_object.py similarity index 100% rename from tests/a_docs/kafka/publisher_object/test_publisher_object.py rename to tests/docs/kafka/publisher_object/test_publisher_object.py diff --git a/tests/a_docs/rabbit/subscription/__init__.py b/tests/docs/kafka/raw_publish/__init__.py similarity index 100% rename from tests/a_docs/rabbit/subscription/__init__.py rename to tests/docs/kafka/raw_publish/__init__.py diff --git a/tests/a_docs/kafka/raw_publish/test_raw_publish.py b/tests/docs/kafka/raw_publish/test_raw_publish.py similarity index 100% rename from tests/a_docs/kafka/raw_publish/test_raw_publish.py rename to tests/docs/kafka/raw_publish/test_raw_publish.py diff --git a/tests/docs/kafka/test_security.py b/tests/docs/kafka/test_security.py new file mode 100644 index 0000000000..41d862a431 --- /dev/null +++ b/tests/docs/kafka/test_security.py @@ -0,0 +1,124 @@ +import ssl +from contextlib import contextmanager +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + + +@contextmanager +def patch_aio_consumer_and_producer() -> tuple[MagicMock, MagicMock]: + try: + producer = MagicMock(return_value=AsyncMock()) + admin_client = MagicMock(return_value=AsyncMock()) + + with ( + patch("aiokafka.AIOKafkaProducer", new=producer), + patch("aiokafka.admin.client.AIOKafkaAdminClient", new=admin_client), + ): + yield producer + finally: + pass + + +@pytest.mark.asyncio() +@pytest.mark.kafka() +async def test_base_security() -> None: + from docs.docs_src.kafka.security.basic import broker as basic_broker + + with patch_aio_consumer_and_producer() as producer: + async with basic_broker: + producer_call_kwargs = producer.call_args.kwargs + + call_kwargs = {} + call_kwargs["security_protocol"] = "SSL" + + assert call_kwargs.items() <= producer_call_kwargs.items() + + assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext + + +@pytest.mark.asyncio() +@pytest.mark.kafka() +async def test_scram256() -> None: + from docs.docs_src.kafka.security.sasl_scram256 import ( + broker as scram256_broker, + ) + + with patch_aio_consumer_and_producer() as producer: + async with scram256_broker: + producer_call_kwargs = producer.call_args.kwargs + + call_kwargs = {} + call_kwargs["sasl_mechanism"] = "SCRAM-SHA-256" + call_kwargs["sasl_plain_username"] = "admin" + call_kwargs["sasl_plain_password"] = "password" # pragma: allowlist secret + call_kwargs["security_protocol"] = "SASL_SSL" + + assert call_kwargs.items() <= producer_call_kwargs.items() + + assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext + + +@pytest.mark.asyncio() +@pytest.mark.kafka() +async def test_scram512() -> None: + from docs.docs_src.kafka.security.sasl_scram512 import ( + broker as scram512_broker, + ) + + with patch_aio_consumer_and_producer() as producer: + async with scram512_broker: + producer_call_kwargs = producer.call_args.kwargs + + call_kwargs = {} + call_kwargs["sasl_mechanism"] = "SCRAM-SHA-512" + call_kwargs["sasl_plain_username"] = "admin" + call_kwargs["sasl_plain_password"] = "password" # pragma: allowlist secret + call_kwargs["security_protocol"] = "SASL_SSL" + + assert call_kwargs.items() <= producer_call_kwargs.items() + + assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext + + +@pytest.mark.asyncio() +@pytest.mark.kafka() +async def test_plaintext() -> None: + from docs.docs_src.kafka.security.plaintext import ( + broker as plaintext_broker, + ) + + with patch_aio_consumer_and_producer() as producer: + async with plaintext_broker: + producer_call_kwargs = producer.call_args.kwargs + + call_kwargs = {} + call_kwargs["sasl_mechanism"] = "PLAIN" + call_kwargs["sasl_plain_username"] = "admin" + call_kwargs["sasl_plain_password"] = "password" # pragma: allowlist secret + call_kwargs["security_protocol"] = "SASL_SSL" + + assert call_kwargs.items() <= producer_call_kwargs.items() + + assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext + + +@pytest.mark.kafka() +@pytest.mark.asyncio() +async def test_gssapi() -> None: + from docs.docs_src.kafka.security.sasl_gssapi import ( + broker as gssapi_broker, + ) + + with patch_aio_consumer_and_producer() as producer: + async with gssapi_broker: + producer_call_kwargs = producer.call_args.kwargs + + call_kwargs = { + "sasl_mechanism": "GSSAPI", + "security_protocol": "SASL_SSL", + } + + assert call_kwargs.items() <= producer_call_kwargs.items() + + assert type(producer_call_kwargs["ssl_context"]) is ssl.SSLContext diff --git a/tests/docs/nats/__init__.py b/tests/docs/nats/__init__.py new file mode 100644 index 0000000000..87ead90ee6 --- /dev/null +++ b/tests/docs/nats/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("nats") diff --git a/tests/a_docs/redis/list/__init__.py b/tests/docs/nats/ack/__init__.py similarity index 100% rename from tests/a_docs/redis/list/__init__.py rename to tests/docs/nats/ack/__init__.py diff --git a/tests/docs/nats/ack/test_errors.py b/tests/docs/nats/ack/test_errors.py new file mode 100644 index 0000000000..1c91969794 --- /dev/null +++ b/tests/docs/nats/ack/test_errors.py @@ -0,0 +1,19 @@ +from unittest.mock import patch + +import pytest +from nats.aio.msg import Msg + +from faststream.nats import TestApp, TestNatsBroker +from tests.tools import spy_decorator + + +@pytest.mark.asyncio() +@pytest.mark.nats() +async def test_ack_exc() -> None: + from docs.docs_src.nats.ack.errors import app, broker, handle + + with patch.object(Msg, "ack", spy_decorator(Msg.ack)) as m: + async with TestNatsBroker(broker, with_real=True), TestApp(app): + await handle.wait_call(3) + + assert m.mock.call_count diff --git a/tests/a_docs/redis/pub_sub/__init__.py b/tests/docs/nats/js/__init__.py similarity index 100% rename from tests/a_docs/redis/pub_sub/__init__.py rename to tests/docs/nats/js/__init__.py diff --git a/tests/docs/nats/js/test_kv.py b/tests/docs/nats/js/test_kv.py new file mode 100644 index 0000000000..dd340fff33 --- /dev/null +++ b/tests/docs/nats/js/test_kv.py @@ -0,0 +1,15 @@ +import pytest + +from faststream import TestApp +from faststream.nats import TestNatsBroker + + +@pytest.mark.asyncio() +@pytest.mark.nats() +@pytest.mark.flaky(retries=3, retry_delay=1) +async def test_basic() -> None: + from docs.docs_src.nats.js.key_value import app, broker, handler + + async with TestNatsBroker(broker, with_real=True), TestApp(app): + await handler.wait_call(3.0) + handler.mock.assert_called_once_with(b"Hello!") diff --git a/tests/a_docs/nats/js/test_main.py b/tests/docs/nats/js/test_main.py similarity index 82% rename from tests/a_docs/nats/js/test_main.py rename to tests/docs/nats/js/test_main.py index 0972d938ad..70eef4f7fa 100644 --- a/tests/a_docs/nats/js/test_main.py +++ b/tests/docs/nats/js/test_main.py @@ -4,8 +4,8 @@ from faststream.nats import TestNatsBroker -@pytest.mark.asyncio -async def test_main(): +@pytest.mark.asyncio() +async def test_main() -> None: from docs.docs_src.nats.js.main import app, broker, handler async with TestNatsBroker(broker), TestApp(app): diff --git a/tests/docs/nats/js/test_object.py b/tests/docs/nats/js/test_object.py new file mode 100644 index 0000000000..ac7dbade90 --- /dev/null +++ b/tests/docs/nats/js/test_object.py @@ -0,0 +1,17 @@ +import pytest + +from faststream import TestApp +from faststream.nats import TestNatsBroker + + +@pytest.mark.asyncio() +@pytest.mark.nats() +async def test_basic() -> None: + from docs.docs_src.nats.js.object import app, broker, handler + + async with ( + TestNatsBroker(broker, with_real=True, connect_only=True), + TestApp(app), + ): + await handler.wait_call(3.0) + handler.mock.assert_called_once_with("file.txt") diff --git a/tests/a_docs/nats/js/test_pull_sub.py b/tests/docs/nats/js/test_pull_sub.py similarity index 83% rename from tests/a_docs/nats/js/test_pull_sub.py rename to tests/docs/nats/js/test_pull_sub.py index a9fc8a5919..989508467d 100644 --- a/tests/a_docs/nats/js/test_pull_sub.py +++ b/tests/docs/nats/js/test_pull_sub.py @@ -3,8 +3,8 @@ from faststream.nats import TestApp, TestNatsBroker -@pytest.mark.asyncio -async def test_basic(): +@pytest.mark.asyncio() +async def test_basic() -> None: from docs.docs_src.nats.js.pull_sub import app, broker, handle async with TestNatsBroker(broker), TestApp(app): diff --git a/tests/docs/nats/test_direct.py b/tests/docs/nats/test_direct.py new file mode 100644 index 0000000000..6ca15a5f5a --- /dev/null +++ b/tests/docs/nats/test_direct.py @@ -0,0 +1,19 @@ +import pytest + +from faststream.nats import TestApp, TestNatsBroker + + +@pytest.mark.asyncio() +async def test_direct() -> None: + from docs.docs_src.nats.direct import ( + app, + base_handler1, + base_handler2, + base_handler3, + broker, + ) + + async with TestNatsBroker(broker), TestApp(app): + assert base_handler1.mock.call_count == 2 + assert base_handler2.mock.call_count == 0 + assert base_handler3.mock.call_count == 1 diff --git a/tests/a_docs/nats/test_pattern.py b/tests/docs/nats/test_pattern.py similarity index 88% rename from tests/a_docs/nats/test_pattern.py rename to tests/docs/nats/test_pattern.py index b00029d3b0..9be3baddca 100644 --- a/tests/a_docs/nats/test_pattern.py +++ b/tests/docs/nats/test_pattern.py @@ -3,8 +3,8 @@ from faststream.nats import TestApp, TestNatsBroker -@pytest.mark.asyncio -async def test_pattern(): +@pytest.mark.asyncio() +async def test_pattern() -> None: from docs.docs_src.nats.pattern import ( app, base_handler1, diff --git a/tests/docs/rabbit/__init__.py b/tests/docs/rabbit/__init__.py new file mode 100644 index 0000000000..ebec43fcd5 --- /dev/null +++ b/tests/docs/rabbit/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("aio_pika") diff --git a/tests/a_docs/redis/stream/__init__.py b/tests/docs/rabbit/ack/__init__.py similarity index 100% rename from tests/a_docs/redis/stream/__init__.py rename to tests/docs/rabbit/ack/__init__.py diff --git a/tests/docs/rabbit/ack/test_errors.py b/tests/docs/rabbit/ack/test_errors.py new file mode 100644 index 0000000000..b930a77964 --- /dev/null +++ b/tests/docs/rabbit/ack/test_errors.py @@ -0,0 +1,19 @@ +from unittest.mock import patch + +import pytest +from aio_pika import IncomingMessage + +from faststream.rabbit import TestApp, TestRabbitBroker +from tests.tools import spy_decorator + + +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_ack_exc() -> None: + from docs.docs_src.rabbit.ack.errors import app, broker, handle + + with patch.object(IncomingMessage, "ack", spy_decorator(IncomingMessage.ack)) as m: + async with TestRabbitBroker(broker, with_real=True), TestApp(app): + await handle.wait_call(3) + + m.mock.assert_called_once() diff --git a/tests/docs/rabbit/subscription/__init__.py b/tests/docs/rabbit/subscription/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/docs/rabbit/subscription/test_direct.py b/tests/docs/rabbit/subscription/test_direct.py new file mode 100644 index 0000000000..948dc8e0cb --- /dev/null +++ b/tests/docs/rabbit/subscription/test_direct.py @@ -0,0 +1,17 @@ +import pytest + +from faststream.rabbit import TestApp, TestRabbitBroker + + +@pytest.mark.asyncio() +async def test_index() -> None: + from docs.docs_src.rabbit.subscription.direct import ( + app, + base_handler1, + base_handler3, + broker, + ) + + async with TestRabbitBroker(broker), TestApp(app): + base_handler1.mock.assert_called_with(b"") + base_handler3.mock.assert_called_once_with(b"") diff --git a/tests/a_docs/rabbit/subscription/test_fanout.py b/tests/docs/rabbit/subscription/test_fanout.py similarity index 85% rename from tests/a_docs/rabbit/subscription/test_fanout.py rename to tests/docs/rabbit/subscription/test_fanout.py index 29dc6c2cd3..70761ec2f6 100644 --- a/tests/a_docs/rabbit/subscription/test_fanout.py +++ b/tests/docs/rabbit/subscription/test_fanout.py @@ -3,9 +3,9 @@ from faststream.rabbit import TestApp, TestRabbitBroker -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_index(): +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_index() -> None: from docs.docs_src.rabbit.subscription.fanout import ( app, base_handler1, diff --git a/tests/a_docs/rabbit/subscription/test_header.py b/tests/docs/rabbit/subscription/test_header.py similarity index 87% rename from tests/a_docs/rabbit/subscription/test_header.py rename to tests/docs/rabbit/subscription/test_header.py index 1b96b26a19..ccea7663cc 100644 --- a/tests/a_docs/rabbit/subscription/test_header.py +++ b/tests/docs/rabbit/subscription/test_header.py @@ -3,8 +3,8 @@ from faststream.rabbit import TestApp, TestRabbitBroker -@pytest.mark.asyncio -async def test_index(): +@pytest.mark.asyncio() +async def test_index() -> None: from docs.docs_src.rabbit.subscription.header import ( app, base_handler1, diff --git a/tests/a_docs/rabbit/subscription/test_index.py b/tests/docs/rabbit/subscription/test_index.py similarity index 82% rename from tests/a_docs/rabbit/subscription/test_index.py rename to tests/docs/rabbit/subscription/test_index.py index 185ab942e5..09d1a0eacf 100644 --- a/tests/a_docs/rabbit/subscription/test_index.py +++ b/tests/docs/rabbit/subscription/test_index.py @@ -3,8 +3,8 @@ from faststream.rabbit import TestApp, TestRabbitBroker -@pytest.mark.asyncio -async def test_index(): +@pytest.mark.asyncio() +async def test_index() -> None: from docs.docs_src.rabbit.subscription.index import app, broker, handle async with TestRabbitBroker(broker), TestApp(app): diff --git a/tests/a_docs/rabbit/subscription/test_stream.py b/tests/docs/rabbit/subscription/test_stream.py similarity index 79% rename from tests/a_docs/rabbit/subscription/test_stream.py rename to tests/docs/rabbit/subscription/test_stream.py index 80e244ca1f..381ecf40ce 100644 --- a/tests/a_docs/rabbit/subscription/test_stream.py +++ b/tests/docs/rabbit/subscription/test_stream.py @@ -3,9 +3,9 @@ from faststream.rabbit import TestApp, TestRabbitBroker -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_stream(): +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_stream() -> None: from docs.docs_src.rabbit.subscription.stream import app, broker, handle async with TestRabbitBroker(broker, with_real=True), TestApp(app): diff --git a/tests/a_docs/rabbit/subscription/test_topic.py b/tests/docs/rabbit/subscription/test_topic.py similarity index 86% rename from tests/a_docs/rabbit/subscription/test_topic.py rename to tests/docs/rabbit/subscription/test_topic.py index 45e2f51c9f..28d9590605 100644 --- a/tests/a_docs/rabbit/subscription/test_topic.py +++ b/tests/docs/rabbit/subscription/test_topic.py @@ -3,8 +3,8 @@ from faststream.rabbit import TestApp, TestRabbitBroker -@pytest.mark.asyncio -async def test_index(): +@pytest.mark.asyncio() +async def test_index() -> None: from docs.docs_src.rabbit.subscription.topic import ( app, base_handler1, diff --git a/tests/docs/rabbit/test_bind.py b/tests/docs/rabbit/test_bind.py new file mode 100644 index 0000000000..7415369d0b --- /dev/null +++ b/tests/docs/rabbit/test_bind.py @@ -0,0 +1,30 @@ +from unittest.mock import AsyncMock + +import pytest +from aio_pika import RobustQueue + +from faststream import TestApp +from tests.marks import require_aiopika + + +@pytest.mark.asyncio() +@pytest.mark.rabbit() +@require_aiopika +async def test_bind(monkeypatch, async_mock: AsyncMock): + from docs.docs_src.rabbit.bind import app, broker, some_exchange, some_queue + + with monkeypatch.context() as m: + m.setattr(RobustQueue, "bind", async_mock) + + async with TestApp(app): + assert len(broker.config.declarer._queues) == 2 # with `reply-to` + assert len(broker.config.declarer._exchanges) == 1 + + assert some_queue in broker.config.declarer._queues + assert some_exchange in broker.config.declarer._exchanges + + row_exchange = await broker.config.declarer.declare_exchange(some_exchange) + async_mock.assert_awaited_once_with( + exchange=row_exchange, + routing_key=some_queue.name, + ) diff --git a/tests/docs/rabbit/test_declare.py b/tests/docs/rabbit/test_declare.py new file mode 100644 index 0000000000..688129eaea --- /dev/null +++ b/tests/docs/rabbit/test_declare.py @@ -0,0 +1,13 @@ +import pytest + +from faststream import TestApp + + +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_declare() -> None: + from docs.docs_src.rabbit.declare import app, broker + + async with TestApp(app): + assert len(broker.config.declarer._exchanges) == 1 + assert len(broker.config.declarer._queues) == 2 # with `reply-to` diff --git a/tests/docs/rabbit/test_security.py b/tests/docs/rabbit/test_security.py new file mode 100644 index 0000000000..86bbd1252b --- /dev/null +++ b/tests/docs/rabbit/test_security.py @@ -0,0 +1,65 @@ +import pytest +from aiormq.exceptions import AMQPConnectionError + +from faststream.specification.asyncapi import AsyncAPI + + +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_base_security() -> None: + from docs.docs_src.rabbit.security.basic import broker + + with pytest.raises(AMQPConnectionError): + async with broker: + pass + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}, "securitySchemes": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "amqps", + "protocolVersion": "0.9.1", + "security": [], + "url": "amqps://guest:guest@localhost:5672/", # pragma: allowlist secret + }, + }, + } + + +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_plaintext_security() -> None: + from docs.docs_src.rabbit.security.plaintext import broker + + with pytest.raises(AMQPConnectionError): + async with broker: + pass + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + assert ( + schema + == { + "asyncapi": "2.6.0", + "channels": {}, + "components": { + "messages": {}, + "schemas": {}, + "securitySchemes": {"user-password": {"type": "userPassword"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "amqps", + "protocolVersion": "0.9.1", + "security": [{"user-password": []}], + "url": "amqps://admin:password@localhost:5672/", # pragma: allowlist secret + }, + }, + } + ) diff --git a/tests/docs/redis/__init__.py b/tests/docs/redis/__init__.py new file mode 100644 index 0000000000..4752ef19b1 --- /dev/null +++ b/tests/docs/redis/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("redis") diff --git a/tests/docs/redis/list/__init__.py b/tests/docs/redis/list/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/docs/redis/list/test_list_pub.py b/tests/docs/redis/list/test_list_pub.py new file mode 100644 index 0000000000..cf3b094882 --- /dev/null +++ b/tests/docs/redis/list/test_list_pub.py @@ -0,0 +1,15 @@ +import pytest + +from faststream.redis import TestRedisBroker + + +@pytest.mark.asyncio() +async def test_list_publisher() -> None: + from docs.docs_src.redis.list.list_pub import broker, on_input_data + + publisher = list(broker._publishers)[0] # noqa: RUF015 + + async with TestRedisBroker(broker) as br: + await br.publish({"data": 1.0}, list="input-list") + on_input_data.mock.assert_called_once_with({"data": 1.0}) + publisher.mock.assert_called_once_with({"data": 2.0}) diff --git a/tests/a_docs/redis/list/test_list_sub.py b/tests/docs/redis/list/test_list_sub.py similarity index 83% rename from tests/a_docs/redis/list/test_list_sub.py rename to tests/docs/redis/list/test_list_sub.py index 30ec9320af..b9d8e66e03 100644 --- a/tests/a_docs/redis/list/test_list_sub.py +++ b/tests/docs/redis/list/test_list_sub.py @@ -3,8 +3,8 @@ from faststream.redis import TestRedisBroker -@pytest.mark.asyncio -async def test_list(): +@pytest.mark.asyncio() +async def test_list() -> None: from docs.docs_src.redis.list.list_sub import broker, handle async with TestRedisBroker(broker) as br: diff --git a/tests/a_docs/redis/list/test_sub_batch.py b/tests/docs/redis/list/test_sub_batch.py similarity index 77% rename from tests/a_docs/redis/list/test_sub_batch.py rename to tests/docs/redis/list/test_sub_batch.py index 2bceec8aa8..9137fd1cd4 100644 --- a/tests/a_docs/redis/list/test_sub_batch.py +++ b/tests/docs/redis/list/test_sub_batch.py @@ -1,12 +1,10 @@ import pytest from faststream.redis import TestRedisBroker -from tests.marks import python39 -@pytest.mark.asyncio -@python39 -async def test_batch(): +@pytest.mark.asyncio() +async def test_batch() -> None: from docs.docs_src.redis.list.sub_batch import broker, handle async with TestRedisBroker(broker) as br: diff --git a/tests/docs/redis/pub_sub/__init__.py b/tests/docs/redis/pub_sub/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/a_docs/redis/pub_sub/test_channel_sub.py b/tests/docs/redis/pub_sub/test_channel_sub.py similarity index 82% rename from tests/a_docs/redis/pub_sub/test_channel_sub.py rename to tests/docs/redis/pub_sub/test_channel_sub.py index 8a01f62acc..1f756024c2 100644 --- a/tests/a_docs/redis/pub_sub/test_channel_sub.py +++ b/tests/docs/redis/pub_sub/test_channel_sub.py @@ -3,8 +3,8 @@ from faststream.redis import TestRedisBroker -@pytest.mark.asyncio -async def test_channel(): +@pytest.mark.asyncio() +async def test_channel() -> None: from docs.docs_src.redis.pub_sub.channel_sub import broker, handle async with TestRedisBroker(broker) as br: diff --git a/tests/a_docs/redis/pub_sub/test_channel_sub_pattern.py b/tests/docs/redis/pub_sub/test_channel_sub_pattern.py similarity index 83% rename from tests/a_docs/redis/pub_sub/test_channel_sub_pattern.py rename to tests/docs/redis/pub_sub/test_channel_sub_pattern.py index a4bc91236f..cb0bbf8b47 100644 --- a/tests/a_docs/redis/pub_sub/test_channel_sub_pattern.py +++ b/tests/docs/redis/pub_sub/test_channel_sub_pattern.py @@ -3,8 +3,8 @@ from faststream.redis import TestRedisBroker -@pytest.mark.asyncio -async def test_pattern(): +@pytest.mark.asyncio() +async def test_pattern() -> None: from docs.docs_src.redis.pub_sub.channel_sub_pattern import broker, handle_test async with TestRedisBroker(broker) as br: diff --git a/tests/a_docs/redis/pub_sub/test_pattern_data.py b/tests/docs/redis/pub_sub/test_pattern_data.py similarity index 82% rename from tests/a_docs/redis/pub_sub/test_pattern_data.py rename to tests/docs/redis/pub_sub/test_pattern_data.py index 3f12374b6e..b532f1b2f9 100644 --- a/tests/a_docs/redis/pub_sub/test_pattern_data.py +++ b/tests/docs/redis/pub_sub/test_pattern_data.py @@ -3,8 +3,8 @@ from faststream.redis import TestRedisBroker -@pytest.mark.asyncio -async def test_pattern_data(): +@pytest.mark.asyncio() +async def test_pattern_data() -> None: from docs.docs_src.redis.pub_sub.pattern_data import broker, handle_test async with TestRedisBroker(broker) as br: diff --git a/tests/a_docs/redis/pub_sub/test_publihser_object.py b/tests/docs/redis/pub_sub/test_publihser_object.py similarity index 100% rename from tests/a_docs/redis/pub_sub/test_publihser_object.py rename to tests/docs/redis/pub_sub/test_publihser_object.py diff --git a/tests/a_docs/redis/pub_sub/test_publisher_decorator.py b/tests/docs/redis/pub_sub/test_publisher_decorator.py similarity index 87% rename from tests/a_docs/redis/pub_sub/test_publisher_decorator.py rename to tests/docs/redis/pub_sub/test_publisher_decorator.py index 0a15552556..c5af28db57 100644 --- a/tests/a_docs/redis/pub_sub/test_publisher_decorator.py +++ b/tests/docs/redis/pub_sub/test_publisher_decorator.py @@ -3,8 +3,8 @@ from faststream.redis import TestRedisBroker -@pytest.mark.asyncio -async def test_publisher(): +@pytest.mark.asyncio() +async def test_publisher() -> None: from docs.docs_src.redis.pub_sub.publisher_decorator import ( broker, on_input_data, diff --git a/tests/a_docs/redis/pub_sub/test_raw_publish.py b/tests/docs/redis/pub_sub/test_raw_publish.py similarity index 100% rename from tests/a_docs/redis/pub_sub/test_raw_publish.py rename to tests/docs/redis/pub_sub/test_raw_publish.py diff --git a/tests/docs/redis/stream/__init__.py b/tests/docs/redis/stream/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/a_docs/redis/stream/test_ack_errors.py b/tests/docs/redis/stream/test_ack_errors.py similarity index 86% rename from tests/a_docs/redis/stream/test_ack_errors.py rename to tests/docs/redis/stream/test_ack_errors.py index 2e76294916..3adb5bc280 100644 --- a/tests/a_docs/redis/stream/test_ack_errors.py +++ b/tests/docs/redis/stream/test_ack_errors.py @@ -7,9 +7,9 @@ from tests.tools import spy_decorator -@pytest.mark.redis -@pytest.mark.asyncio -async def test_stream_ack(): +@pytest.mark.redis() +@pytest.mark.asyncio() +async def test_stream_ack() -> None: from docs.docs_src.redis.stream.ack_errors import app, broker, handle with patch.object(Redis, "xack", spy_decorator(Redis.xack)) as m: diff --git a/tests/docs/redis/stream/test_batch_sub.py b/tests/docs/redis/stream/test_batch_sub.py new file mode 100644 index 0000000000..74a196d5a9 --- /dev/null +++ b/tests/docs/redis/stream/test_batch_sub.py @@ -0,0 +1,12 @@ +import pytest + +from faststream.redis import TestRedisBroker + + +@pytest.mark.asyncio() +async def test_stream_batch() -> None: + from docs.docs_src.redis.stream.batch_sub import broker, handle + + async with TestRedisBroker(broker) as br: + await br.publish("Hi!", stream="test-stream") + handle.mock.assert_called_once_with(["Hi!"]) diff --git a/tests/a_docs/redis/stream/test_group.py b/tests/docs/redis/stream/test_group.py similarity index 79% rename from tests/a_docs/redis/stream/test_group.py rename to tests/docs/redis/stream/test_group.py index 7573a28312..d8c620cf70 100644 --- a/tests/a_docs/redis/stream/test_group.py +++ b/tests/docs/redis/stream/test_group.py @@ -3,8 +3,8 @@ from faststream.redis import TestApp, TestRedisBroker -@pytest.mark.asyncio -async def test_stream_group(): +@pytest.mark.asyncio() +async def test_stream_group() -> None: from docs.docs_src.redis.stream.group import app, broker, handle async with TestRedisBroker(broker), TestApp(app): diff --git a/tests/a_docs/redis/stream/test_pub.py b/tests/docs/redis/stream/test_pub.py similarity index 75% rename from tests/a_docs/redis/stream/test_pub.py rename to tests/docs/redis/stream/test_pub.py index 0656267f7b..cc4d51cbfa 100644 --- a/tests/a_docs/redis/stream/test_pub.py +++ b/tests/docs/redis/stream/test_pub.py @@ -3,11 +3,11 @@ from faststream.redis import TestRedisBroker -@pytest.mark.asyncio -async def test_stream_pub(): +@pytest.mark.asyncio() +async def test_stream_pub() -> None: from docs.docs_src.redis.stream.pub import broker, on_input_data - publisher = list(broker._publishers.values())[0] # noqa: RUF015 + publisher = list(broker._publishers)[0] # noqa: RUF015 async with TestRedisBroker(broker) as br: await br.publish({"data": 1.0}, stream="input-stream") diff --git a/tests/a_docs/redis/stream/test_sub.py b/tests/docs/redis/stream/test_sub.py similarity index 82% rename from tests/a_docs/redis/stream/test_sub.py rename to tests/docs/redis/stream/test_sub.py index f3d002b6fb..be4c9a993a 100644 --- a/tests/a_docs/redis/stream/test_sub.py +++ b/tests/docs/redis/stream/test_sub.py @@ -3,8 +3,8 @@ from faststream.redis import TestRedisBroker -@pytest.mark.asyncio -async def test_stream_sub(): +@pytest.mark.asyncio() +async def test_stream_sub() -> None: from docs.docs_src.redis.stream.sub import broker, handle async with TestRedisBroker(broker) as br: diff --git a/tests/docs/redis/test_pipeline.py b/tests/docs/redis/test_pipeline.py new file mode 100644 index 0000000000..c2cf989cf7 --- /dev/null +++ b/tests/docs/redis/test_pipeline.py @@ -0,0 +1,16 @@ +import pytest + +from faststream.redis import TestApp, TestRedisBroker + + +@pytest.mark.asyncio() +async def test_pipeline() -> None: + from docs.docs_src.redis.pipeline.pipeline import ( + app, + broker, + handle, + ) + + broker.config.fd_config.serializer = None + async with TestRedisBroker(broker), TestApp(app): + handle.mock.assert_called_once_with("Hi!") diff --git a/tests/docs/redis/test_rpc.py b/tests/docs/redis/test_rpc.py new file mode 100644 index 0000000000..5784e8b2fd --- /dev/null +++ b/tests/docs/redis/test_rpc.py @@ -0,0 +1,14 @@ +import pytest + +from faststream.redis import TestApp, TestRedisBroker + + +@pytest.mark.asyncio() +async def test_rpc() -> None: + from docs.docs_src.redis.rpc.app import ( + app, + broker, + ) + + async with TestRedisBroker(broker), TestApp(app): + pass diff --git a/tests/docs/redis/test_security.py b/tests/docs/redis/test_security.py new file mode 100644 index 0000000000..c13630c808 --- /dev/null +++ b/tests/docs/redis/test_security.py @@ -0,0 +1,89 @@ +from contextlib import contextmanager +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from redis.exceptions import AuthenticationError + +from faststream.specification.asyncapi import AsyncAPI + + +@contextmanager +def patch_asyncio_open_connection() -> tuple[MagicMock, MagicMock]: + try: + reader = MagicMock() + reader.readline = AsyncMock(return_value=b":1\r\n") + reader.read = AsyncMock(return_value=b"") + + writer = MagicMock() + writer.drain = AsyncMock() + writer.wait_closed = AsyncMock() + + open_connection = AsyncMock(return_value=(reader, writer)) + + with patch("asyncio.open_connection", new=open_connection): + yield open_connection + finally: + pass + + +@pytest.mark.asyncio() +@pytest.mark.redis() +async def test_base_security() -> None: + with patch_asyncio_open_connection() as connection: + from docs.docs_src.redis.security.basic import broker + + async with broker: + await broker.ping(0.01) + + assert connection.call_args.kwargs["ssl"] + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": {"messages": {}, "schemas": {}, "securitySchemes": {}}, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "redis", + "protocolVersion": "custom", + "security": [], + "url": "redis://localhost:6379", + }, + }, + } + + +@pytest.mark.asyncio() +@pytest.mark.redis() +async def test_plaintext_security() -> None: + with patch_asyncio_open_connection() as connection: + from docs.docs_src.redis.security.plaintext import broker + + with pytest.raises(AuthenticationError): + async with broker: + await broker._connection.ping() + + assert connection.call_args.kwargs["ssl"] + + schema = AsyncAPI(broker, schema_version="2.6.0").to_jsonable() + assert schema == { + "asyncapi": "2.6.0", + "channels": {}, + "components": { + "messages": {}, + "schemas": {}, + "securitySchemes": {"user-password": {"type": "userPassword"}}, + }, + "defaultContentType": "application/json", + "info": {"description": "", "title": "FastStream", "version": "0.1.0"}, + "servers": { + "development": { + "protocol": "redis", + "protocolVersion": "custom", + "security": [{"user-password": []}], + "url": "redis://localhost:6379", + }, + }, + } diff --git a/tests/examples/fastapi_integration/test_app.py b/tests/examples/fastapi_integration/test_app.py index 24fb0bd6ac..3f4e69e582 100644 --- a/tests/examples/fastapi_integration/test_app.py +++ b/tests/examples/fastapi_integration/test_app.py @@ -3,22 +3,26 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_handler(): - from examples.fastapi_integration.testing import router - from examples.fastapi_integration.testing import test_handler as test_ +async def test_handler() -> None: + from examples.fastapi_integration.testing import ( + router, + test_handler as test_, + ) from faststream.rabbit import TestRabbitBroker async with TestRabbitBroker(router.broker) as br: await test_(br) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_incorrect(): - from examples.fastapi_integration.testing import router - from examples.fastapi_integration.testing import test_incorrect as test_ +async def test_incorrect() -> None: + from examples.fastapi_integration.testing import ( + router, + test_incorrect as test_, + ) from faststream.rabbit import TestRabbitBroker async with TestRabbitBroker(router.broker) as br: diff --git a/tests/examples/kafka/test_ack.py b/tests/examples/kafka/test_ack.py index 064bfd4130..c2402f2f6a 100644 --- a/tests/examples/kafka/test_ack.py +++ b/tests/examples/kafka/test_ack.py @@ -4,12 +4,14 @@ from examples.kafka.ack_after_process import app, broker from faststream.kafka import TestApp, TestKafkaBroker -from faststream.kafka.message import KafkaMessage +from faststream.kafka.message import KafkaAckableMessage from tests.tools import spy_decorator -@pytest.mark.asyncio -async def test_ack(): - with patch.object(KafkaMessage, "ack", spy_decorator(KafkaMessage.ack)) as m: +@pytest.mark.asyncio() +async def test_ack() -> None: + with patch.object( + KafkaAckableMessage, "ack", spy_decorator(KafkaAckableMessage.ack) + ) as m: async with TestKafkaBroker(broker), TestApp(app): m.mock.assert_called_once() diff --git a/tests/examples/kafka/test_batch_consume.py b/tests/examples/kafka/test_batch_consume.py index 0f1dade28c..53d61197bc 100644 --- a/tests/examples/kafka/test_batch_consume.py +++ b/tests/examples/kafka/test_batch_consume.py @@ -4,8 +4,8 @@ from faststream.kafka import TestApp, TestKafkaBroker -@pytest.mark.asyncio -async def test_example(): +@pytest.mark.asyncio() +async def test_example() -> None: async with TestKafkaBroker(broker), TestApp(app): await handle.wait_call(3) assert set(handle.mock.call_args[0][0]) == {"hi", "FastStream"} diff --git a/tests/examples/kafka/test_batch_publish_1.py b/tests/examples/kafka/test_batch_publish_1.py index 88bee705ee..015019cbe4 100644 --- a/tests/examples/kafka/test_batch_publish_1.py +++ b/tests/examples/kafka/test_batch_publish_1.py @@ -4,8 +4,8 @@ from faststream.kafka import TestApp, TestKafkaBroker -@pytest.mark.asyncio -async def test_example(): +@pytest.mark.asyncio() +async def test_example() -> None: async with TestKafkaBroker(broker), TestApp(app): await handle.wait_call(3) assert set(handle.mock.call_args[0][0]) == {"hi", "FastStream"} diff --git a/tests/examples/kafka/test_batch_publish_2.py b/tests/examples/kafka/test_batch_publish_2.py index 974e279eab..3c66d26ff2 100644 --- a/tests/examples/kafka/test_batch_publish_2.py +++ b/tests/examples/kafka/test_batch_publish_2.py @@ -4,8 +4,8 @@ from faststream.kafka import TestApp, TestKafkaBroker -@pytest.mark.asyncio -async def test_example(): +@pytest.mark.asyncio() +async def test_example() -> None: async with TestKafkaBroker(broker), TestApp(app): await handle.wait_call(3) assert set(handle.mock.call_args[0][0]) == {"hi", "FastStream"} diff --git a/tests/examples/kafka/test_batch_publish_3.py b/tests/examples/kafka/test_batch_publish_3.py index 62d91d0389..e453e4e5ec 100644 --- a/tests/examples/kafka/test_batch_publish_3.py +++ b/tests/examples/kafka/test_batch_publish_3.py @@ -4,8 +4,8 @@ from faststream.kafka import TestApp, TestKafkaBroker -@pytest.mark.asyncio -async def test_example(): +@pytest.mark.asyncio() +async def test_example() -> None: async with TestKafkaBroker(broker), TestApp(app): await handle.wait_call(3) await handle_response.wait_call(3) diff --git a/tests/examples/nats/test_e01_basic.py b/tests/examples/nats/test_e01_basic.py index cd487c6184..fefb49c621 100644 --- a/tests/examples/nats/test_e01_basic.py +++ b/tests/examples/nats/test_e01_basic.py @@ -4,8 +4,8 @@ from faststream.nats import TestNatsBroker -@pytest.mark.asyncio -async def test_basic(): +@pytest.mark.asyncio() +async def test_basic() -> None: from examples.nats.e01_basic import app, broker, handler async with TestNatsBroker(broker), TestApp(app): diff --git a/tests/examples/nats/test_e02_basic_rpc.py b/tests/examples/nats/test_e02_basic_rpc.py index f7a5e81100..28e64e46c2 100644 --- a/tests/examples/nats/test_e02_basic_rpc.py +++ b/tests/examples/nats/test_e02_basic_rpc.py @@ -4,8 +4,8 @@ from faststream.nats import TestNatsBroker -@pytest.mark.asyncio -async def test_basic(): +@pytest.mark.asyncio() +async def test_basic() -> None: from examples.nats.e02_basic_rpc import app, broker, handler async with TestNatsBroker(broker), TestApp(app): diff --git a/tests/examples/nats/test_e03_publisher.py b/tests/examples/nats/test_e03_publisher.py index 870552189a..7bb17ec340 100644 --- a/tests/examples/nats/test_e03_publisher.py +++ b/tests/examples/nats/test_e03_publisher.py @@ -4,8 +4,8 @@ from faststream.nats import TestNatsBroker -@pytest.mark.asyncio -async def test_basic(): +@pytest.mark.asyncio() +async def test_basic() -> None: from examples.nats.e03_publisher import app, broker, handler, response_handler async with TestNatsBroker(broker), TestApp(app): diff --git a/tests/examples/nats/test_e04_js_basic.py b/tests/examples/nats/test_e04_js_basic.py index b806b5ad51..f89865b110 100644 --- a/tests/examples/nats/test_e04_js_basic.py +++ b/tests/examples/nats/test_e04_js_basic.py @@ -4,8 +4,8 @@ from faststream.nats import TestNatsBroker -@pytest.mark.asyncio -async def test_basic(): +@pytest.mark.asyncio() +async def test_basic() -> None: from examples.nats.e04_js_basic import app, broker, handler async with TestNatsBroker(broker), TestApp(app): diff --git a/tests/examples/nats/test_e05_basic_and_js.py b/tests/examples/nats/test_e05_basic_and_js.py index 3be0c91c4b..a94d260371 100644 --- a/tests/examples/nats/test_e05_basic_and_js.py +++ b/tests/examples/nats/test_e05_basic_and_js.py @@ -4,8 +4,8 @@ from faststream.nats import TestNatsBroker -@pytest.mark.asyncio -async def test_basic(): +@pytest.mark.asyncio() +async def test_basic() -> None: from examples.nats.e05_basic_and_js import app, broker, core_handler, js_handler async with TestNatsBroker(broker), TestApp(app): diff --git a/tests/examples/nats/test_e06_key_value.py b/tests/examples/nats/test_e06_key_value.py index b8150b9ef6..e1977bbaf4 100644 --- a/tests/examples/nats/test_e06_key_value.py +++ b/tests/examples/nats/test_e06_key_value.py @@ -4,9 +4,9 @@ from faststream.nats import TestNatsBroker -@pytest.mark.asyncio -@pytest.mark.nats -async def test_basic(): +@pytest.mark.asyncio() +@pytest.mark.nats() +async def test_basic() -> None: from examples.nats.e06_key_value import app, broker, handler async with TestNatsBroker(broker, with_real=True), TestApp(app): diff --git a/tests/examples/nats/test_e07_object_storage.py b/tests/examples/nats/test_e07_object_storage.py index c8783daaa0..bd2a0dfcb2 100644 --- a/tests/examples/nats/test_e07_object_storage.py +++ b/tests/examples/nats/test_e07_object_storage.py @@ -4,9 +4,9 @@ from faststream.nats import TestNatsBroker -@pytest.mark.asyncio -@pytest.mark.nats -async def test_basic(): +@pytest.mark.asyncio() +@pytest.mark.nats() +async def test_basic() -> None: from examples.nats.e07_object_storage import app, broker, handler async with TestNatsBroker(broker, with_real=True): diff --git a/tests/examples/nats/test_e08_wildcards.py b/tests/examples/nats/test_e08_wildcards.py index 38680dc7a9..b970866a65 100644 --- a/tests/examples/nats/test_e08_wildcards.py +++ b/tests/examples/nats/test_e08_wildcards.py @@ -4,8 +4,8 @@ from faststream.nats import TestNatsBroker -@pytest.mark.asyncio -async def test_basic(): +@pytest.mark.asyncio() +async def test_basic() -> None: from examples.nats.e08_wildcards import app, broker, handler, handler_match async with TestNatsBroker(broker), TestApp(app): diff --git a/tests/examples/nats/test_e09_pull_sub.py b/tests/examples/nats/test_e09_pull_sub.py index 67ff897278..198470db5c 100644 --- a/tests/examples/nats/test_e09_pull_sub.py +++ b/tests/examples/nats/test_e09_pull_sub.py @@ -3,8 +3,8 @@ from faststream.nats import TestApp, TestNatsBroker -@pytest.mark.asyncio -async def test_basic(): +@pytest.mark.asyncio() +async def test_basic() -> None: from examples.nats.e09_pull_sub import app, broker, handle async with TestNatsBroker(broker), TestApp(app): diff --git a/tests/examples/rabbit/test_direct.py b/tests/examples/rabbit/test_direct.py index 8407924274..10a91fbff0 100644 --- a/tests/examples/rabbit/test_direct.py +++ b/tests/examples/rabbit/test_direct.py @@ -3,8 +3,8 @@ from faststream.rabbit import TestApp, TestRabbitBroker -@pytest.mark.asyncio -async def test_index(): +@pytest.mark.asyncio() +async def test_index() -> None: from examples.rabbit.direct import ( app, base_handler1, diff --git a/tests/examples/rabbit/test_fanout.py b/tests/examples/rabbit/test_fanout.py index f758c46176..f1d200425f 100644 --- a/tests/examples/rabbit/test_fanout.py +++ b/tests/examples/rabbit/test_fanout.py @@ -3,9 +3,9 @@ from faststream.rabbit import TestApp, TestRabbitBroker -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_index(): +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_index() -> None: from examples.rabbit.fanout import ( app, base_handler1, diff --git a/tests/examples/rabbit/test_header.py b/tests/examples/rabbit/test_header.py index 7fd0beb36e..8c2786913b 100644 --- a/tests/examples/rabbit/test_header.py +++ b/tests/examples/rabbit/test_header.py @@ -3,8 +3,8 @@ from faststream.rabbit import TestApp, TestRabbitBroker -@pytest.mark.asyncio -async def test_index(): +@pytest.mark.asyncio() +async def test_index() -> None: from examples.rabbit.header import ( app, base_handler1, diff --git a/tests/examples/rabbit/test_stream.py b/tests/examples/rabbit/test_stream.py index 4cb91a7d71..8273babccf 100644 --- a/tests/examples/rabbit/test_stream.py +++ b/tests/examples/rabbit/test_stream.py @@ -3,9 +3,9 @@ from faststream.rabbit import TestApp, TestRabbitBroker -@pytest.mark.asyncio -@pytest.mark.rabbit -async def test_stream(): +@pytest.mark.asyncio() +@pytest.mark.rabbit() +async def test_stream() -> None: from examples.rabbit.stream import app, broker, handle async with TestRabbitBroker(broker, with_real=True), TestApp(app): diff --git a/tests/examples/rabbit/test_topic.py b/tests/examples/rabbit/test_topic.py index a3458ad8d5..ff327cbd97 100644 --- a/tests/examples/rabbit/test_topic.py +++ b/tests/examples/rabbit/test_topic.py @@ -3,8 +3,8 @@ from faststream.rabbit import TestApp, TestRabbitBroker -@pytest.mark.asyncio -async def test_index(): +@pytest.mark.asyncio() +async def test_index() -> None: from examples.rabbit.topic import ( app, base_handler1, diff --git a/tests/examples/redis/test_channel_sub.py b/tests/examples/redis/test_channel_sub.py index 5e97e69885..7de2522448 100644 --- a/tests/examples/redis/test_channel_sub.py +++ b/tests/examples/redis/test_channel_sub.py @@ -3,8 +3,8 @@ from faststream.redis import TestApp, TestRedisBroker -@pytest.mark.asyncio -async def test_channel(): +@pytest.mark.asyncio() +async def test_channel() -> None: from examples.redis.channel_sub import app, broker, handle async with TestRedisBroker(broker), TestApp(app): diff --git a/tests/examples/redis/test_channel_sub_pattern.py b/tests/examples/redis/test_channel_sub_pattern.py index 4e29dfa1f1..80a59fdfa8 100644 --- a/tests/examples/redis/test_channel_sub_pattern.py +++ b/tests/examples/redis/test_channel_sub_pattern.py @@ -3,8 +3,8 @@ from faststream.redis import TestApp, TestRedisBroker -@pytest.mark.asyncio -async def test_pattern(): +@pytest.mark.asyncio() +async def test_pattern() -> None: from examples.redis.channel_sub_pattern import app, broker, handle_test async with TestRedisBroker(broker), TestApp(app): diff --git a/tests/examples/redis/test_list_sub.py b/tests/examples/redis/test_list_sub.py index 4d81a471ba..c2e427dfa2 100644 --- a/tests/examples/redis/test_list_sub.py +++ b/tests/examples/redis/test_list_sub.py @@ -3,8 +3,8 @@ from faststream.redis import TestApp, TestRedisBroker -@pytest.mark.asyncio -async def test_list(): +@pytest.mark.asyncio() +async def test_list() -> None: from examples.redis.list_sub import app, broker, handle async with TestRedisBroker(broker), TestApp(app): diff --git a/tests/examples/redis/test_list_sub_batch.py b/tests/examples/redis/test_list_sub_batch.py index 3450c4366d..c70d382b61 100644 --- a/tests/examples/redis/test_list_sub_batch.py +++ b/tests/examples/redis/test_list_sub_batch.py @@ -1,12 +1,10 @@ import pytest from faststream.redis import TestApp, TestRedisBroker -from tests.marks import python39 -@pytest.mark.asyncio -@python39 -async def test_batch(): +@pytest.mark.asyncio() +async def test_batch() -> None: from examples.redis.list_sub_batch import app, broker, handle async with TestRedisBroker(broker), TestApp(app): diff --git a/tests/examples/redis/test_rpc.py b/tests/examples/redis/test_rpc.py index cc71bc2081..e7ec71a34f 100644 --- a/tests/examples/redis/test_rpc.py +++ b/tests/examples/redis/test_rpc.py @@ -3,8 +3,8 @@ from faststream.redis import TestApp, TestRedisBroker -@pytest.mark.asyncio -async def test_rpc(): +@pytest.mark.asyncio() +async def test_rpc() -> None: from examples.redis.rpc import ( app, broker, diff --git a/tests/examples/redis/test_stream_batch_sub.py b/tests/examples/redis/test_stream_batch_sub.py index f9871ff99f..2086cbff9f 100644 --- a/tests/examples/redis/test_stream_batch_sub.py +++ b/tests/examples/redis/test_stream_batch_sub.py @@ -1,12 +1,10 @@ import pytest from faststream.redis import TestApp, TestRedisBroker -from tests.marks import python39 -@pytest.mark.asyncio -@python39 -async def test_stream_batch(): +@pytest.mark.asyncio() +async def test_stream_batch() -> None: from examples.redis.stream_sub_batch import app, broker, handle async with TestRedisBroker(broker), TestApp(app): diff --git a/tests/examples/redis/test_stream_sub.py b/tests/examples/redis/test_stream_sub.py index bf260013f5..761914fd66 100644 --- a/tests/examples/redis/test_stream_sub.py +++ b/tests/examples/redis/test_stream_sub.py @@ -3,8 +3,8 @@ from faststream.redis import TestApp, TestRedisBroker -@pytest.mark.asyncio -async def test_stream_sub(): +@pytest.mark.asyncio() +async def test_stream_sub() -> None: from examples.redis.stream_sub import app, broker, handle async with TestRedisBroker(broker), TestApp(app): diff --git a/tests/examples/router/test_basic_consume.py b/tests/examples/router/test_basic_consume.py index 07475f0e56..3a5fcb4044 100644 --- a/tests/examples/router/test_basic_consume.py +++ b/tests/examples/router/test_basic_consume.py @@ -4,8 +4,8 @@ from faststream.kafka import TestApp, TestKafkaBroker -@pytest.mark.asyncio -async def test_example(): +@pytest.mark.asyncio() +async def test_example() -> None: async with TestKafkaBroker(broker), TestApp(app): await handle.wait_call(3) diff --git a/tests/examples/router/test_basic_publish.py b/tests/examples/router/test_basic_publish.py index 973dec7982..0b5224e556 100644 --- a/tests/examples/router/test_basic_publish.py +++ b/tests/examples/router/test_basic_publish.py @@ -4,8 +4,8 @@ from faststream.kafka import TestApp, TestKafkaBroker -@pytest.mark.asyncio -async def test_example(): +@pytest.mark.asyncio() +async def test_example() -> None: async with TestKafkaBroker(broker), TestApp(app): await handle.wait_call(3) await handle_response.wait_call(3) diff --git a/tests/examples/router/test_delay_registration.py b/tests/examples/router/test_delay_registration.py index dc8197b8bd..1d2f34e798 100644 --- a/tests/examples/router/test_delay_registration.py +++ b/tests/examples/router/test_delay_registration.py @@ -4,9 +4,9 @@ from faststream.kafka import TestApp, TestKafkaBroker -@pytest.mark.asyncio -async def test_example(): - sub = next(iter(broker._subscribers.values())) +@pytest.mark.asyncio() +async def test_example() -> None: + sub = broker.subscribers[0] sub.topic = "prefix_in" handle = sub.calls[0].handler diff --git a/tests/examples/test_e01_basic_consume.py b/tests/examples/test_e01_basic_consume.py index 70157fc328..0ca69e1333 100644 --- a/tests/examples/test_e01_basic_consume.py +++ b/tests/examples/test_e01_basic_consume.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_example(): +async def test_example() -> None: from examples.e01_basic_consume import app, broker, handle from faststream.rabbit import TestApp, TestRabbitBroker diff --git a/tests/examples/test_e02_1_basic_publisher.py b/tests/examples/test_e02_1_basic_publisher.py index b6c1b5d703..6ba632668b 100644 --- a/tests/examples/test_e02_1_basic_publisher.py +++ b/tests/examples/test_e02_1_basic_publisher.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_example(): +async def test_example() -> None: from examples.e02_1_basic_publisher import app, broker, handle, handle_response from faststream.rabbit import TestApp, TestRabbitBroker diff --git a/tests/examples/test_e02_2_basic_publisher.py b/tests/examples/test_e02_2_basic_publisher.py index 38498dc1bd..94ce18164b 100644 --- a/tests/examples/test_e02_2_basic_publisher.py +++ b/tests/examples/test_e02_2_basic_publisher.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_example(): +async def test_example() -> None: from examples.e02_2_basic_publisher import app, broker, handle, handle_response from faststream.rabbit import TestApp, TestRabbitBroker diff --git a/tests/examples/test_e02_3_basic_publisher.py b/tests/examples/test_e02_3_basic_publisher.py index 73664f7641..9842332f64 100644 --- a/tests/examples/test_e02_3_basic_publisher.py +++ b/tests/examples/test_e02_3_basic_publisher.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_example(): +async def test_example() -> None: from examples.e02_3_basic_publisher import app, broker, handle, handle_response from faststream.rabbit import TestApp, TestRabbitBroker diff --git a/tests/examples/test_e03_miltiple_pubsub.py b/tests/examples/test_e03_miltiple_pubsub.py index 4e624343d0..fbcecf0004 100644 --- a/tests/examples/test_e03_miltiple_pubsub.py +++ b/tests/examples/test_e03_miltiple_pubsub.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_example(): +async def test_example() -> None: from examples.e03_miltiple_pubsub import ( app, broker, diff --git a/tests/examples/test_e04_msg_filter.py b/tests/examples/test_e04_msg_filter.py index 489d672bac..08dc87f59c 100644 --- a/tests/examples/test_e04_msg_filter.py +++ b/tests/examples/test_e04_msg_filter.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_example(): +async def test_example() -> None: from examples.e04_msg_filter import app, broker, handle_json, handle_other_messages from faststream.rabbit import TestApp, TestRabbitBroker diff --git a/tests/examples/test_e05_rpc_request.py b/tests/examples/test_e05_rpc_request.py index b005bda0f8..a72f1be7c6 100644 --- a/tests/examples/test_e05_rpc_request.py +++ b/tests/examples/test_e05_rpc_request.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_example(): +async def test_example() -> None: from examples.e05_rpc_request import app, broker, handle from faststream.rabbit import TestApp, TestRabbitBroker diff --git a/tests/examples/test_e06_manual_ack.py b/tests/examples/test_e06_manual_ack.py index 4272c15663..9fc261b837 100644 --- a/tests/examples/test_e06_manual_ack.py +++ b/tests/examples/test_e06_manual_ack.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_example(): +async def test_example() -> None: from examples.e06_manual_ack import app, broker, handle from faststream.rabbit import TestApp, TestRabbitBroker diff --git a/tests/examples/test_e07_ack_immediately.py b/tests/examples/test_e07_ack_immediately.py index 3413ec320c..fb701b1a0b 100644 --- a/tests/examples/test_e07_ack_immediately.py +++ b/tests/examples/test_e07_ack_immediately.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_example(): +async def test_example() -> None: from examples.e07_ack_immediately import app, broker, handle from faststream.rabbit import TestApp, TestRabbitBroker diff --git a/tests/examples/test_e08_testing.py b/tests/examples/test_e08_testing.py index 52613be0c3..fb72181dc2 100644 --- a/tests/examples/test_e08_testing.py +++ b/tests/examples/test_e08_testing.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_handle(): +async def test_handle() -> None: from examples.e08_testing import test_handle as _test await _test() diff --git a/tests/examples/test_e09_testing_mocks.py b/tests/examples/test_e09_testing_mocks.py index 83b097f186..1a26328258 100644 --- a/tests/examples/test_e09_testing_mocks.py +++ b/tests/examples/test_e09_testing_mocks.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_handle(): +async def test_handle() -> None: from examples.e09_testing_mocks import test_handle as _test await _test() diff --git a/tests/examples/test_e10_middlewares.py b/tests/examples/test_e10_middlewares.py index f6f1220392..b2db14d434 100644 --- a/tests/examples/test_e10_middlewares.py +++ b/tests/examples/test_e10_middlewares.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_example(): +async def test_example() -> None: from examples.e10_middlewares import app, broker, handle from faststream.rabbit import TestApp, TestRabbitBroker diff --git a/tests/examples/test_e11_settings.py b/tests/examples/test_e11_settings.py index 5992637beb..8e2a19d8af 100644 --- a/tests/examples/test_e11_settings.py +++ b/tests/examples/test_e11_settings.py @@ -3,9 +3,9 @@ from tests.marks import require_aiopika -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_example(): +async def test_example() -> None: from examples.e11_settings import app, broker, handle from faststream.rabbit import TestApp, TestRabbitBroker diff --git a/tests/integrations/__init__.py b/tests/integrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/msgspec/__init__.py b/tests/integrations/msgspec/__init__.py new file mode 100644 index 0000000000..eb3dc84cf3 --- /dev/null +++ b/tests/integrations/msgspec/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("msgspec") diff --git a/tests/integrations/msgspec/test_serialization.py b/tests/integrations/msgspec/test_serialization.py new file mode 100644 index 0000000000..ca371bc246 --- /dev/null +++ b/tests/integrations/msgspec/test_serialization.py @@ -0,0 +1,83 @@ +from typing import Any +from unittest.mock import MagicMock + +import msgspec +import pytest +from fast_depends.msgspec import MsgSpecSerializer + +from faststream._internal.broker.broker import BrokerUsecase +from faststream._internal.testing.broker import TestBroker +from faststream.confluent import ( + KafkaBroker as ConfluentBroker, + TestKafkaBroker as TestConfluentBroker, +) +from faststream.kafka import KafkaBroker, TestKafkaBroker +from faststream.nats import NatsBroker, TestNatsBroker +from faststream.rabbit import RabbitBroker, TestRabbitBroker +from faststream.redis import RedisBroker, TestRedisBroker +from tests.brokers.base.publish import parametrized + + +class SimpleModel(msgspec.Struct): + r: str + + +@pytest.mark.asyncio() +@pytest.mark.parametrize( + ("message", "message_type", "expected_message"), + ( + *parametrized, + pytest.param( + msgspec.json.encode(SimpleModel(r="hello!")), + SimpleModel, + SimpleModel(r="hello!"), + id="bytes->model", + ), + pytest.param( + SimpleModel(r="hello!"), + SimpleModel, + SimpleModel(r="hello!"), + id="model->model", + ), + pytest.param( + SimpleModel(r="hello!"), + dict, + {"r": "hello!"}, + id="model->dict", + ), + pytest.param( + {"r": "hello!"}, + SimpleModel, + SimpleModel(r="hello!"), + id="dict->model", + ), + ), +) +@pytest.mark.parametrize( + ("broker_cls", "test_cls"), + ( + pytest.param(RabbitBroker, TestRabbitBroker, id="rabbit"), + pytest.param(RedisBroker, TestRedisBroker, id="redis"), + pytest.param(KafkaBroker, TestKafkaBroker, id="kafka"), + pytest.param(NatsBroker, TestNatsBroker, id="nats"), + pytest.param(ConfluentBroker, TestConfluentBroker, id="confluent"), + ), +) +async def test_msgspec_serialize( + message: Any, + message_type: Any, + expected_message: Any, + mock: MagicMock, + broker_cls: type[BrokerUsecase[Any, Any]], + test_cls: type[TestBroker[Any]], +) -> None: + broker = broker_cls(serializer=MsgSpecSerializer) + + @broker.subscriber("test") + async def handler(m: message_type) -> None: + mock(m) + + async with test_cls(broker) as br: + await br.publish(message, "test") + + mock.assert_called_with(expected_message) diff --git a/tests/log/test_formatter.py b/tests/log/test_formatter.py index e8f21a9e52..792e0d7380 100644 --- a/tests/log/test_formatter.py +++ b/tests/log/test_formatter.py @@ -1,10 +1,10 @@ import logging -from faststream.log.formatter import ColourizedFormatter +from faststream._internal.logger.formatter import ColourizedFormatter -def test_formatter(): - logger = logging.getLogger(__file__) +def test_formatter() -> None: + logger = logging.getLogger(__name__) handler = logging.Handler() formatter = ColourizedFormatter("%(message)s") handler.setFormatter(formatter) diff --git a/tests/log/test_logging.py b/tests/log/test_logging.py index 0782b825b1..30787f4b02 100644 --- a/tests/log/test_logging.py +++ b/tests/log/test_logging.py @@ -1,17 +1,18 @@ import logging +import sys from io import StringIO from unittest.mock import patch -from faststream.log.logging import set_logger_fmt +from faststream._internal.logger.logging import set_logger_fmt -def test_duplicates_set_formatter(): - logger = logging.getLogger(__file__) +def test_duplicates_set_formatter() -> None: + logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) log_output = StringIO() with patch("sys.stdout", log_output): - set_logger_fmt(logger, fmt="%(message)s with format1") - set_logger_fmt(logger, fmt="%(message)s with format2") + set_logger_fmt(logger, stream=sys.stdout, fmt="%(message)s with format1") + set_logger_fmt(logger, stream=sys.stdout, fmt="%(message)s with format2") logger.info("msg") assert log_output.getvalue().strip() == "msg with format1" diff --git a/tests/marks.py b/tests/marks.py index 07bde035b0..bd86d72ff4 100644 --- a/tests/marks.py +++ b/tests/marks.py @@ -1,17 +1,10 @@ -import sys - import pytest -from faststream._compat import PYDANTIC_V2 - -python39 = pytest.mark.skipif( - sys.version_info < (3, 9), - reason="requires python3.9+", -) +from faststream._internal._compat import IS_WINDOWS, PYDANTIC_V2 -python310 = pytest.mark.skipif( - sys.version_info < (3, 10), - reason="requires python3.10+", +skip_windows = pytest.mark.skipif( + IS_WINDOWS, + reason="does not run on windows", ) pydantic_v1 = pytest.mark.skipif( diff --git a/tests/mocks.py b/tests/mocks.py index 3482444b68..fc3d8b5cf6 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -1,16 +1,18 @@ +from collections.abc import Mapping from contextlib import contextmanager -from typing import Any, Mapping +from typing import Any from unittest.mock import Mock from pytest import MonkeyPatch # noqa: PT013 @contextmanager -def mock_pydantic_settings_env(env_mapping: Mapping[str, Any]): +def mock_pydantic_settings_env(env_mapping: Mapping[str, Any]) -> None: with MonkeyPatch().context() as c: mock = Mock() mock.return_value = env_mapping c.setattr( - "pydantic_settings.sources.DotEnvSettingsSource._read_env_files", mock + "pydantic_settings.sources.DotEnvSettingsSource._read_env_files", + mock, ) yield diff --git a/tests/mypy/kafka.py b/tests/mypy/kafka.py index eeeef066ed..aee20036f5 100644 --- a/tests/mypy/kafka.py +++ b/tests/mypy/kafka.py @@ -1,10 +1,13 @@ -from typing import Awaitable, Callable +from collections.abc import Awaitable, Callable +import prometheus_client from aiokafka import ConsumerRecord +from faststream._internal.basic_types import DecodedMessage from faststream.kafka import KafkaBroker, KafkaMessage, KafkaRoute, KafkaRouter from faststream.kafka.fastapi import KafkaRouter as FastAPIRouter -from faststream.types import DecodedMessage +from faststream.kafka.opentelemetry import KafkaTelemetryMiddleware +from faststream.kafka.prometheus import KafkaPrometheusMiddleware def sync_decoder(msg: KafkaMessage) -> DecodedMessage: @@ -16,7 +19,8 @@ async def async_decoder(msg: KafkaMessage) -> DecodedMessage: async def custom_decoder( - msg: KafkaMessage, original: Callable[[KafkaMessage], Awaitable[DecodedMessage]] + msg: KafkaMessage, + original: Callable[[KafkaMessage], Awaitable[DecodedMessage]], ) -> DecodedMessage: return await original(msg) @@ -27,15 +31,16 @@ async def custom_decoder( def sync_parser(msg: ConsumerRecord) -> KafkaMessage: - return "" # type: ignore + return "" # type: ignore[return-value] async def async_parser(msg: ConsumerRecord) -> KafkaMessage: - return "" # type: ignore + return "" # type: ignore[return-value] async def custom_parser( - msg: ConsumerRecord, original: Callable[[ConsumerRecord], Awaitable[KafkaMessage]] + msg: ConsumerRecord, + original: Callable[[ConsumerRecord], Awaitable[KafkaMessage]], ) -> KafkaMessage: return await original(msg) @@ -197,7 +202,7 @@ def async_handler() -> None: ... parser=custom_parser, decoder=custom_decoder, ), - ) + ), ) @@ -263,3 +268,13 @@ def handle20() -> None: ... @fastapi_router.subscriber("test") @fastapi_router.publisher("test2") async def handle21() -> None: ... + + +otlp_middleware = KafkaTelemetryMiddleware() +KafkaBroker().add_middleware(otlp_middleware) +KafkaBroker(middlewares=[otlp_middleware]) + + +prometheus_middleware = KafkaPrometheusMiddleware(registry=prometheus_client.REGISTRY) +KafkaBroker().add_middleware(prometheus_middleware) +KafkaBroker(middlewares=[prometheus_middleware]) diff --git a/tests/mypy/nats.py b/tests/mypy/nats.py index 955458eada..1b5ef4bfe9 100644 --- a/tests/mypy/nats.py +++ b/tests/mypy/nats.py @@ -1,10 +1,13 @@ -from typing import Awaitable, Callable +from collections.abc import Awaitable, Callable +import prometheus_client from nats.aio.msg import Msg +from faststream._internal.basic_types import DecodedMessage from faststream.nats import NatsBroker, NatsMessage, NatsRoute, NatsRouter from faststream.nats.fastapi import NatsRouter as FastAPIRouter -from faststream.types import DecodedMessage +from faststream.nats.opentelemetry import NatsTelemetryMiddleware +from faststream.nats.prometheus import NatsPrometheusMiddleware def sync_decoder(msg: NatsMessage) -> DecodedMessage: @@ -16,7 +19,8 @@ async def async_decoder(msg: NatsMessage) -> DecodedMessage: async def custom_decoder( - msg: NatsMessage, original: Callable[[NatsMessage], Awaitable[DecodedMessage]] + msg: NatsMessage, + original: Callable[[NatsMessage], Awaitable[DecodedMessage]], ) -> DecodedMessage: return await original(msg) @@ -27,15 +31,16 @@ async def custom_decoder( def sync_parser(msg: Msg) -> NatsMessage: - return "" # type: ignore + return "" # type: ignore[return-value] async def async_parser(msg: Msg) -> NatsMessage: - return "" # type: ignore + return "" # type: ignore[return-value] async def custom_parser( - msg: Msg, original: Callable[[Msg], Awaitable[NatsMessage]] + msg: Msg, + original: Callable[[Msg], Awaitable[NatsMessage]], ) -> NatsMessage: return await original(msg) @@ -198,7 +203,7 @@ def async_handler() -> None: ... parser=custom_parser, decoder=custom_decoder, ), - ) + ), ) @@ -264,3 +269,13 @@ def handle20() -> None: ... @fastapi_router.subscriber("test") @fastapi_router.publisher("test2") async def handle21() -> None: ... + + +otlp_middleware = NatsTelemetryMiddleware() +NatsBroker().add_middleware(otlp_middleware) +NatsBroker(middlewares=[otlp_middleware]) + + +prometheus_middleware = NatsPrometheusMiddleware(registry=prometheus_client.REGISTRY) +NatsBroker().add_middleware(prometheus_middleware) +NatsBroker(middlewares=[prometheus_middleware]) diff --git a/tests/mypy/rabbit.py b/tests/mypy/rabbit.py index 064f6faad7..a456e06d50 100644 --- a/tests/mypy/rabbit.py +++ b/tests/mypy/rabbit.py @@ -1,10 +1,13 @@ -from typing import Awaitable, Callable +from collections.abc import Awaitable, Callable +import prometheus_client from aio_pika import IncomingMessage +from faststream._internal.basic_types import DecodedMessage from faststream.rabbit import RabbitBroker, RabbitMessage, RabbitRoute, RabbitRouter from faststream.rabbit.fastapi import RabbitRouter as FastAPIRouter -from faststream.types import DecodedMessage +from faststream.rabbit.opentelemetry import RabbitTelemetryMiddleware +from faststream.rabbit.prometheus import RabbitPrometheusMiddleware def sync_decoder(msg: RabbitMessage) -> DecodedMessage: @@ -16,7 +19,8 @@ async def async_decoder(msg: RabbitMessage) -> DecodedMessage: async def custom_decoder( - msg: RabbitMessage, original: Callable[[RabbitMessage], Awaitable[DecodedMessage]] + msg: RabbitMessage, + original: Callable[[RabbitMessage], Awaitable[DecodedMessage]], ) -> DecodedMessage: return await original(msg) @@ -27,11 +31,11 @@ async def custom_decoder( def sync_parser(msg: IncomingMessage) -> RabbitMessage: - return "" # type: ignore + return "" # type: ignore[return-value] async def async_parser(msg: IncomingMessage) -> RabbitMessage: - return "" # type: ignore + return "" # type: ignore[return-value] async def custom_parser( @@ -198,7 +202,7 @@ def async_handler() -> None: ... parser=custom_parser, decoder=custom_decoder, ), - ) + ), ) @@ -265,3 +269,13 @@ def handle20() -> None: ... @fastapi_router.subscriber("test") @fastapi_router.publisher("test2") async def handle21() -> None: ... + + +otlp_middleware = RabbitTelemetryMiddleware() +RabbitBroker().add_middleware(otlp_middleware) +RabbitBroker(middlewares=[otlp_middleware]) + + +prometheus_middleware = RabbitPrometheusMiddleware(registry=prometheus_client.REGISTRY) +RabbitBroker().add_middleware(prometheus_middleware) +RabbitBroker(middlewares=[prometheus_middleware]) diff --git a/tests/mypy/redis.py b/tests/mypy/redis.py index 58a3da36cd..4555fc4432 100644 --- a/tests/mypy/redis.py +++ b/tests/mypy/redis.py @@ -1,12 +1,18 @@ -from typing import Awaitable, Callable +from collections.abc import Awaitable, Callable -from faststream.redis import RedisBroker as Broker -from faststream.redis import RedisMessage as Message -from faststream.redis import RedisRoute as Route -from faststream.redis import RedisRouter as StreamRouter +import prometheus_client + +from faststream._internal.basic_types import DecodedMessage +from faststream.redis import ( + RedisBroker as Broker, + RedisMessage as Message, + RedisRoute as Route, + RedisRouter as StreamRouter, +) from faststream.redis.fastapi import RedisRouter as FastAPIRouter from faststream.redis.message import RedisMessage as Msg -from faststream.types import DecodedMessage +from faststream.redis.opentelemetry import RedisTelemetryMiddleware +from faststream.redis.prometheus import RedisPrometheusMiddleware def sync_decoder(msg: Message) -> DecodedMessage: @@ -18,7 +24,8 @@ async def async_decoder(msg: Message) -> DecodedMessage: async def custom_decoder( - msg: Message, original: Callable[[Message], Awaitable[DecodedMessage]] + msg: Message, + original: Callable[[Message], Awaitable[DecodedMessage]], ) -> DecodedMessage: return await original(msg) @@ -29,15 +36,16 @@ async def custom_decoder( def sync_parser(msg: Msg) -> Message: - return "" # type: ignore + return "" # type: ignore[return-value] async def async_parser(msg: Msg) -> Message: - return "" # type: ignore + return "" # type: ignore[return-value] async def custom_parser( - msg: Msg, original: Callable[[Msg], Awaitable[Message]] + msg: Msg, + original: Callable[[Msg], Awaitable[Message]], ) -> Message: return await original(msg) @@ -201,7 +209,7 @@ def async_handler() -> None: ... parser=custom_parser, decoder=custom_decoder, ), - ) + ), ) @@ -267,3 +275,13 @@ def handle20() -> None: ... @fastapi_router.subscriber("test") @fastapi_router.publisher("test2") async def handle21() -> None: ... + + +otlp_middleware = RedisTelemetryMiddleware() +Broker().add_middleware(otlp_middleware) +Broker(middlewares=[otlp_middleware]) + + +prometheus_middleware = RedisPrometheusMiddleware(registry=prometheus_client.REGISTRY) +Broker().add_middleware(prometheus_middleware) +Broker(middlewares=[prometheus_middleware]) diff --git a/tests/opentelemetry/basic.py b/tests/opentelemetry/basic.py index 1d5b0f4e2e..c457bf8200 100644 --- a/tests/opentelemetry/basic.py +++ b/tests/opentelemetry/basic.py @@ -1,6 +1,6 @@ import asyncio -from typing import List, Optional, Tuple, Type, cast -from unittest.mock import Mock +from typing import Any, cast +from unittest.mock import MagicMock import pytest from dirty_equals import IsFloat, IsUUID @@ -16,41 +16,52 @@ from opentelemetry.semconv.trace import SpanAttributes as SpanAttr from opentelemetry.trace import SpanKind, get_current_span -from faststream.broker.core.usecase import BrokerUsecase +from faststream._internal.broker import BrokerUsecase from faststream.opentelemetry import Baggage, CurrentBaggage, CurrentSpan from faststream.opentelemetry.consts import ( ERROR_TYPE, MESSAGING_DESTINATION_PUBLISH_NAME, ) -from faststream.opentelemetry.middleware import MessageAction as Action -from faststream.opentelemetry.middleware import TelemetryMiddleware +from faststream.opentelemetry.middleware import ( + MessageAction as Action, + TelemetryMiddleware, +) from tests.brokers.base.basic import BaseTestcaseConfig -@pytest.mark.asyncio +@pytest.mark.asyncio() class LocalTelemetryTestcase(BaseTestcaseConfig): messaging_system: str include_messages_counters: bool - broker_class: Type[BrokerUsecase] resource: Resource = Resource.create(attributes={"service.name": "faststream.test"}) - telemetry_middleware_class: TelemetryMiddleware - def patch_broker(self, broker: BrokerUsecase) -> BrokerUsecase: + def get_broker( + self, + apply_types: bool = False, + **kwargs: Any, + ) -> BrokerUsecase[Any, Any]: + raise NotImplementedError + + def patch_broker( + self, + broker: BrokerUsecase[Any, Any], + **kwargs: Any, + ) -> BrokerUsecase[Any, Any]: return broker def destination_name(self, queue: str) -> str: return queue @staticmethod - def get_spans(exporter: InMemorySpanExporter) -> List[Span]: - spans = cast("Tuple[Span, ...]", exporter.get_finished_spans()) - return sorted(spans, key=lambda s: s.start_time) + def get_spans(exporter: InMemorySpanExporter) -> list[Span]: + spans = cast("tuple[Span, ...]", exporter.get_finished_spans()) + return sorted(spans, key=lambda s: s.start_time or 0) @staticmethod def get_metrics( reader: InMemoryMetricReader, - ) -> List[Metric]: + ) -> list[Metric]: """Get sorted metrics. Return order: @@ -62,24 +73,23 @@ def get_metrics( metrics = reader.get_metrics_data() metrics = metrics.resource_metrics[0].scope_metrics[0].metrics metrics = sorted(metrics, key=lambda m: m.name) - return cast("List[Metric]", metrics) + return cast("list[Metric]", metrics) - @pytest.fixture + @pytest.fixture() def tracer_provider(self) -> TracerProvider: - tracer_provider = TracerProvider(resource=self.resource) - return tracer_provider + return TracerProvider(resource=self.resource) - @pytest.fixture + @pytest.fixture() def trace_exporter(self, tracer_provider: TracerProvider) -> InMemorySpanExporter: exporter = InMemorySpanExporter() tracer_provider.add_span_processor(SimpleSpanProcessor(exporter)) return exporter - @pytest.fixture + @pytest.fixture() def metric_reader(self) -> InMemoryMetricReader: return InMemoryMetricReader() - @pytest.fixture + @pytest.fixture() def meter_provider(self, metric_reader: InMemoryMetricReader) -> MeterProvider: return MeterProvider(metric_readers=(metric_reader,), resource=self.resource) @@ -89,9 +99,9 @@ def assert_span( action: str, queue: str, msg: str, - parent_span_id: Optional[str] = None, + parent_span_id: str | None = None, ) -> None: - attrs = span.attributes + attrs = span.attributes or {} assert attrs[SpanAttr.MESSAGING_SYSTEM] == self.messaging_system, attrs[ SpanAttr.MESSAGING_SYSTEM ] @@ -99,14 +109,14 @@ def assert_span( SpanAttr.MESSAGING_MESSAGE_CONVERSATION_ID ] assert span.name == f"{self.destination_name(queue)} {action}", span.name - assert span.kind in (SpanKind.CONSUMER, SpanKind.PRODUCER), span.kind + assert span.kind in {SpanKind.CONSUMER, SpanKind.PRODUCER}, span.kind - if span.kind == SpanKind.PRODUCER and action in (Action.CREATE, Action.PUBLISH): + if span.kind == SpanKind.PRODUCER and action in {Action.CREATE, Action.PUBLISH}: assert attrs[SpanAttr.MESSAGING_DESTINATION_NAME] == queue, attrs[ SpanAttr.MESSAGING_DESTINATION_NAME ] - if span.kind == SpanKind.CONSUMER and action in (Action.CREATE, Action.PROCESS): + if span.kind == SpanKind.CONSUMER and action in {Action.CREATE, Action.PROCESS}: assert attrs[MESSAGING_DESTINATION_PUBLISH_NAME] == queue, attrs[ MESSAGING_DESTINATION_PUBLISH_NAME ] @@ -128,13 +138,14 @@ def assert_span( ] if parent_span_id: + assert span.parent assert span.parent.span_id == parent_span_id, span.parent.span_id def assert_metrics( self, - metrics: List[Metric], + metrics: list[Metric], count: int = 1, - error_type: Optional[str] = None, + error_type: str | None = None, ) -> None: if self.include_messages_counters: assert len(metrics) == 4 @@ -158,19 +169,19 @@ def assert_metrics( async def test_subscriber_create_publish_process_span( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + event: asyncio.Event, + ) -> None: mid = self.telemetry_middleware_class(tracer_provider=tracer_provider) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,)) args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(m): + async def handler(m) -> None: mock(m) event.set() @@ -192,19 +203,18 @@ async def handler(m): self.assert_span(publish, Action.PUBLISH, queue, msg, parent_span_id) self.assert_span(process, Action.PROCESS, queue, msg, parent_span_id) - assert event.is_set() mock.assert_called_once_with(msg) async def test_chain_subscriber_publisher( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + event: asyncio.Event, + ) -> None: mid = self.telemetry_middleware_class(tracer_provider=tracer_provider) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,)) first_queue = queue second_queue = queue + "2" @@ -219,7 +229,7 @@ async def handler1(m): args2, kwargs2 = self.get_subscriber_params(second_queue) @broker.subscriber(*args2, **kwargs2) - async def handler2(m): + async def handler2(m) -> None: mock(m) event.set() @@ -252,35 +262,36 @@ async def handler2(m): < proc2.start_time ) - assert event.is_set() mock.assert_called_once_with(msg) + @pytest.mark.flaky(retries=3, retry_delay=1) async def test_no_trace_context_create_process_span( self, - event: asyncio.Event, queue: str, - mock: Mock, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + event: asyncio.Event, + ) -> None: mid = self.telemetry_middleware_class(tracer_provider=tracer_provider) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,)) + broker_without_middlewares = self.get_broker() args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(m): - mock(m) + async def handler(m) -> None: event.set() broker = self.patch_broker(broker) msg = "start" - async with broker: + async with broker, broker_without_middlewares: await broker.start() - broker._middlewares = () + await broker_without_middlewares.start() + + broker.config.broker_config.middlewares = () tasks = ( - asyncio.create_task(broker.publish(msg, queue)), + asyncio.create_task(broker_without_middlewares.publish(msg, queue)), asyncio.create_task(event.wait()), ) await asyncio.wait(tasks, timeout=self.timeout) @@ -291,24 +302,21 @@ async def handler(m): self.assert_span(create, Action.CREATE, queue, msg) self.assert_span(process, Action.PROCESS, queue, msg, parent_span_id) - assert event.is_set() - mock.assert_called_once_with(msg) - async def test_metrics( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, meter_provider: MeterProvider, metric_reader: InMemoryMetricReader, - ): + event: asyncio.Event, + ) -> None: mid = self.telemetry_middleware_class(meter_provider=meter_provider) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,)) args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(m): + async def handler(m) -> None: mock(m) event.set() @@ -326,26 +334,24 @@ async def handler(m): metrics = self.get_metrics(metric_reader) self.assert_metrics(metrics) - - assert event.is_set() mock.assert_called_once_with(msg) async def test_error_metrics( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, meter_provider: MeterProvider, metric_reader: InMemoryMetricReader, - ): + event: asyncio.Event, + ) -> None: mid = self.telemetry_middleware_class(meter_provider=meter_provider) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,)) expected_value_type = "ValueError" args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(m): + async def handler(m) -> None: try: raise ValueError finally: @@ -366,25 +372,23 @@ async def handler(m): metrics = self.get_metrics(metric_reader) self.assert_metrics(metrics, error_type=expected_value_type) - - assert event.is_set() mock.assert_called_once_with(msg) async def test_span_in_context( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + event: asyncio.Event, + ) -> None: mid = self.telemetry_middleware_class(tracer_provider=tracer_provider) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(m, span: CurrentSpan): + async def handler(m, span: CurrentSpan) -> None: assert span is get_current_span() mock(m) event.set() @@ -400,23 +404,22 @@ async def handler(m, span: CurrentSpan): ) await asyncio.wait(tasks, timeout=self.timeout) - assert event.is_set() mock.assert_called_once_with(msg) async def test_get_baggage( self, - event: asyncio.Event, queue: str, - mock: Mock, - ): + mock: MagicMock, + event: asyncio.Event, + ) -> None: mid = self.telemetry_middleware_class() - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) expected_baggage = {"foo": "bar"} args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler1(m, baggage: CurrentBaggage): + async def handler1(m, baggage: CurrentBaggage) -> None: assert baggage.get("foo") == "bar" assert baggage.get_all() == expected_baggage assert baggage.get_all_batch() == [] @@ -432,24 +435,25 @@ async def handler1(m, baggage: CurrentBaggage): tasks = ( asyncio.create_task( broker.publish( - msg, queue, headers=Baggage({"foo": "bar"}).to_headers() - ) + msg, + queue, + headers=Baggage({"foo": "bar"}).to_headers(), + ), ), asyncio.create_task(event.wait()), ) await asyncio.wait(tasks, timeout=self.timeout) - assert event.is_set() mock.assert_called_once_with(msg) async def test_clear_baggage( self, - event: asyncio.Event, queue: str, - mock: Mock, - ): + mock: MagicMock, + event: asyncio.Event, + ) -> None: mid = self.telemetry_middleware_class() - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) first_queue = queue + "1" second_queue = queue + "2" @@ -466,7 +470,7 @@ async def handler1(m, baggage: CurrentBaggage): args2, kwargs2 = self.get_subscriber_params(second_queue) @broker.subscriber(*args2, **kwargs2) - async def handler2(m, baggage: CurrentBaggage): + async def handler2(m, baggage: CurrentBaggage) -> None: assert baggage.get_all() == {} mock(m) event.set() @@ -479,24 +483,25 @@ async def handler2(m, baggage: CurrentBaggage): tasks = ( asyncio.create_task( broker.publish( - msg, first_queue, headers=Baggage({"foo": "bar"}).to_headers() - ) + msg, + first_queue, + headers=Baggage({"foo": "bar"}).to_headers(), + ), ), asyncio.create_task(event.wait()), ) await asyncio.wait(tasks, timeout=self.timeout) - assert event.is_set() mock.assert_called_once_with(msg) async def test_modify_baggage( self, - event: asyncio.Event, queue: str, - mock: Mock, - ): + mock: MagicMock, + event: asyncio.Event, + ) -> None: mid = self.telemetry_middleware_class() - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) expected_baggage = {"baz": "bar", "bar": "baz"} first_queue = queue + "1" @@ -515,7 +520,7 @@ async def handler1(m, baggage: CurrentBaggage): args2, kwargs2 = self.get_subscriber_params(second_queue) @broker.subscriber(*args2, **kwargs2) - async def handler2(m, baggage: CurrentBaggage): + async def handler2(m, baggage: CurrentBaggage) -> None: assert baggage.get_all() == expected_baggage mock(m) event.set() @@ -528,23 +533,24 @@ async def handler2(m, baggage: CurrentBaggage): tasks = ( asyncio.create_task( broker.publish( - msg, first_queue, headers=Baggage({"foo": "bar"}).to_headers() - ) + msg, + first_queue, + headers=Baggage({"foo": "bar"}).to_headers(), + ), ), asyncio.create_task(event.wait()), ) await asyncio.wait(tasks, timeout=self.timeout) - assert event.is_set() mock.assert_called_once_with(msg) async def test_get_baggage_from_headers( self, - event: asyncio.Event, queue: str, - ): + event: asyncio.Event, + ) -> None: mid = self.telemetry_middleware_class() - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) args, kwargs = self.get_subscriber_params(queue) @@ -559,7 +565,7 @@ async def test_get_baggage_from_headers( propagator.inject(headers, context=ctx) @broker.subscriber(*args, **kwargs) - async def handler(): + async def handler() -> None: baggage_instance = Baggage.from_headers(headers) extracted_baggage = baggage_instance.get_all() assert extracted_baggage == expected_baggage diff --git a/tests/opentelemetry/confluent/test_confluent.py b/tests/opentelemetry/confluent/test_confluent.py index 9eb52d9742..dc71d60a0f 100644 --- a/tests/opentelemetry/confluent/test_confluent.py +++ b/tests/opentelemetry/confluent/test_confluent.py @@ -1,6 +1,6 @@ import asyncio -from typing import Optional -from unittest.mock import Mock +from typing import Any +from unittest.mock import MagicMock import pytest from dirty_equals import IsStr, IsUUID @@ -17,35 +17,36 @@ from faststream.opentelemetry.consts import MESSAGING_DESTINATION_PUBLISH_NAME from faststream.opentelemetry.middleware import MessageAction as Action from tests.brokers.confluent.basic import ConfluentTestcaseConfig +from tests.opentelemetry.basic import LocalTelemetryTestcase -from ..basic import LocalTelemetryTestcase - -@pytest.mark.confluent +@pytest.mark.confluent() class TestTelemetry(ConfluentTestcaseConfig, LocalTelemetryTestcase): messaging_system = "kafka" include_messages_counters = True - broker_class = KafkaBroker telemetry_middleware_class = KafkaTelemetryMiddleware + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> KafkaBroker: + return KafkaBroker(apply_types=apply_types, **kwargs) + def assert_span( self, span: Span, action: str, queue: str, msg: str, - parent_span_id: Optional[str] = None, + parent_span_id: str | None = None, ) -> None: attrs = span.attributes assert attrs[SpanAttr.MESSAGING_SYSTEM] == self.messaging_system assert attrs[SpanAttr.MESSAGING_MESSAGE_CONVERSATION_ID] == IsUUID assert span.name == f"{self.destination_name(queue)} {action}" - assert span.kind in (SpanKind.CONSUMER, SpanKind.PRODUCER) + assert span.kind in {SpanKind.CONSUMER, SpanKind.PRODUCER} - if span.kind == SpanKind.PRODUCER and action in (Action.CREATE, Action.PUBLISH): + if span.kind == SpanKind.PRODUCER and action in {Action.CREATE, Action.PUBLISH}: assert attrs[SpanAttr.MESSAGING_DESTINATION_NAME] == queue - if span.kind == SpanKind.CONSUMER and action in (Action.CREATE, Action.PROCESS): + if span.kind == SpanKind.CONSUMER and action in {Action.CREATE, Action.PROCESS}: assert attrs[MESSAGING_DESTINATION_PUBLISH_NAME] == queue assert attrs[SpanAttr.MESSAGING_MESSAGE_ID] == IsStr(regex=r"0-.+") assert attrs[SpanAttr.MESSAGING_KAFKA_DESTINATION_PARTITION] == 0 @@ -63,48 +64,48 @@ def assert_span( async def test_batch( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, meter_provider: MeterProvider, metric_reader: InMemoryMetricReader, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + ) -> None: + event = asyncio.Event() + mid = self.telemetry_middleware_class( - meter_provider=meter_provider, tracer_provider=tracer_provider + meter_provider=meter_provider, + tracer_provider=tracer_provider, ) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) expected_msg_count = 3 expected_link_count = 1 expected_link_attrs = {"messaging.batch.message_count": 3} expected_baggage = {"with_batch": "True", "foo": "bar"} expected_baggage_batch = [ - {"with_batch": "True", "foo": "bar"} + {"with_batch": "True", "foo": "bar"}, ] * expected_msg_count args, kwargs = self.get_subscriber_params(queue, batch=True) @broker.subscriber(*args, **kwargs) - async def handler(m, baggage: CurrentBaggage): + async def handler(m, baggage: CurrentBaggage) -> None: assert baggage.get_all() == expected_baggage assert baggage.get_all_batch() == expected_baggage_batch mock(m) event.set() - broker = self.patch_broker(broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() tasks = ( asyncio.create_task( - broker.publish_batch( + br.publish_batch( 1, "hi", 3, topic=queue, headers=Baggage({"foo": "bar"}).to_headers(), - ) + ), ), asyncio.create_task(event.wait()), ) @@ -136,11 +137,12 @@ async def test_batch_publish_with_single_consume( metric_reader: InMemoryMetricReader, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + ) -> None: mid = self.telemetry_middleware_class( - meter_provider=meter_provider, tracer_provider=tracer_provider + meter_provider=meter_provider, + tracer_provider=tracer_provider, ) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) msgs_queue = asyncio.Queue(maxsize=3) expected_msg_count = 3 expected_link_count = 1 @@ -151,17 +153,19 @@ async def test_batch_publish_with_single_consume( args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(msg, baggage: CurrentBaggage): + async def handler(msg, baggage: CurrentBaggage) -> None: assert baggage.get_all() == expected_baggage assert baggage.get_all_batch() == [] await msgs_queue.put(msg) - broker = self.patch_broker(broker) - - async with broker: - await broker.start() - await broker.publish_batch( - 1, "hi", 3, topic=queue, headers=Baggage({"foo": "bar"}).to_headers() + async with self.patch_broker(broker) as br: + await br.start() + await br.publish_batch( + 1, + "hi", + 3, + topic=queue, + headers=Baggage({"foo": "bar"}).to_headers(), ) result, _ = await asyncio.wait( ( @@ -195,18 +199,20 @@ async def handler(msg, baggage: CurrentBaggage): async def test_single_publish_with_batch_consume( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, meter_provider: MeterProvider, metric_reader: InMemoryMetricReader, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + ) -> None: + event = asyncio.Event() + mid = self.telemetry_middleware_class( - meter_provider=meter_provider, tracer_provider=tracer_provider + meter_provider=meter_provider, + tracer_provider=tracer_provider, ) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) expected_msg_count = 2 expected_link_count = 2 expected_span_count = 6 @@ -216,27 +222,29 @@ async def test_single_publish_with_batch_consume( args, kwargs = self.get_subscriber_params(queue, batch=True) @broker.subscriber(*args, **kwargs) - async def handler(m, baggage: CurrentBaggage): + async def handler(m, baggage: CurrentBaggage) -> None: assert baggage.get_all() == expected_baggage assert len(baggage.get_all_batch()) == expected_msg_count m.sort() mock(m) event.set() - broker = self.patch_broker(broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() tasks = ( asyncio.create_task( - broker.publish( - "hi", topic=queue, headers=Baggage({"foo": "bar"}).to_headers() - ) + br.publish( + "hi", + topic=queue, + headers=Baggage({"foo": "bar"}).to_headers(), + ), ), asyncio.create_task( - broker.publish( - "buy", topic=queue, headers=Baggage({"bar": "baz"}).to_headers() - ) + br.publish( + "buy", + topic=queue, + headers=Baggage({"bar": "baz"}).to_headers(), + ), ), asyncio.create_task(event.wait()), ) diff --git a/tests/opentelemetry/kafka/test_kafka.py b/tests/opentelemetry/kafka/test_kafka.py index cc38a66281..5d4c785d12 100644 --- a/tests/opentelemetry/kafka/test_kafka.py +++ b/tests/opentelemetry/kafka/test_kafka.py @@ -1,6 +1,6 @@ import asyncio -from typing import Optional -from unittest.mock import Mock +from typing import Any +from unittest.mock import MagicMock import pytest from dirty_equals import IsStr, IsUUID @@ -18,35 +18,36 @@ from faststream.opentelemetry.middleware import MessageAction as Action from tests.brokers.kafka.test_consume import TestConsume from tests.brokers.kafka.test_publish import TestPublish +from tests.opentelemetry.basic import LocalTelemetryTestcase -from ..basic import LocalTelemetryTestcase - -@pytest.mark.kafka +@pytest.mark.kafka() class TestTelemetry(LocalTelemetryTestcase): messaging_system = "kafka" include_messages_counters = True - broker_class = KafkaBroker telemetry_middleware_class = KafkaTelemetryMiddleware + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> KafkaBroker: + return KafkaBroker(apply_types=apply_types, **kwargs) + def assert_span( self, span: Span, action: str, queue: str, msg: str, - parent_span_id: Optional[str] = None, + parent_span_id: str | None = None, ) -> None: attrs = span.attributes assert attrs[SpanAttr.MESSAGING_SYSTEM] == self.messaging_system assert attrs[SpanAttr.MESSAGING_MESSAGE_CONVERSATION_ID] == IsUUID assert span.name == f"{self.destination_name(queue)} {action}" - assert span.kind in (SpanKind.CONSUMER, SpanKind.PRODUCER) + assert span.kind in {SpanKind.CONSUMER, SpanKind.PRODUCER} - if span.kind == SpanKind.PRODUCER and action in (Action.CREATE, Action.PUBLISH): + if span.kind == SpanKind.PRODUCER and action in {Action.CREATE, Action.PUBLISH}: assert attrs[SpanAttr.MESSAGING_DESTINATION_NAME] == queue - if span.kind == SpanKind.CONSUMER and action in (Action.CREATE, Action.PROCESS): + if span.kind == SpanKind.CONSUMER and action in {Action.CREATE, Action.PROCESS}: assert attrs[MESSAGING_DESTINATION_PUBLISH_NAME] == queue assert attrs[SpanAttr.MESSAGING_MESSAGE_ID] == IsStr(regex=r"0-.+") assert attrs[SpanAttr.MESSAGING_KAFKA_DESTINATION_PARTITION] == 0 @@ -64,48 +65,48 @@ def assert_span( async def test_batch( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, meter_provider: MeterProvider, metric_reader: InMemoryMetricReader, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + ) -> None: + event = asyncio.Event() + mid = self.telemetry_middleware_class( - meter_provider=meter_provider, tracer_provider=tracer_provider + meter_provider=meter_provider, + tracer_provider=tracer_provider, ) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) expected_msg_count = 3 expected_link_count = 1 expected_link_attrs = {"messaging.batch.message_count": 3} expected_baggage = {"with_batch": "True", "foo": "bar"} expected_baggage_batch = [ - {"with_batch": "True", "foo": "bar"} + {"with_batch": "True", "foo": "bar"}, ] * expected_msg_count args, kwargs = self.get_subscriber_params(queue, batch=True) @broker.subscriber(*args, **kwargs) - async def handler(m, baggage: CurrentBaggage): + async def handler(m, baggage: CurrentBaggage) -> None: assert baggage.get_all() == expected_baggage assert baggage.get_all_batch() == expected_baggage_batch mock(m) event.set() - broker = self.patch_broker(broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() tasks = ( asyncio.create_task( - broker.publish_batch( + br.publish_batch( 1, "hi", 3, topic=queue, headers=Baggage({"foo": "bar"}).to_headers(), - ) + ), ), asyncio.create_task(event.wait()), ) @@ -137,11 +138,12 @@ async def test_batch_publish_with_single_consume( metric_reader: InMemoryMetricReader, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + ) -> None: mid = self.telemetry_middleware_class( - meter_provider=meter_provider, tracer_provider=tracer_provider + meter_provider=meter_provider, + tracer_provider=tracer_provider, ) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) msgs_queue = asyncio.Queue(maxsize=3) expected_msg_count = 3 expected_link_count = 1 @@ -152,17 +154,19 @@ async def test_batch_publish_with_single_consume( args, kwargs = self.get_subscriber_params(queue) @broker.subscriber(*args, **kwargs) - async def handler(msg, baggage: CurrentBaggage): + async def handler(msg, baggage: CurrentBaggage) -> None: assert baggage.get_all() == expected_baggage assert baggage.get_all_batch() == [] await msgs_queue.put(msg) - broker = self.patch_broker(broker) - - async with broker: - await broker.start() - await broker.publish_batch( - 1, "hi", 3, topic=queue, headers=Baggage({"foo": "bar"}).to_headers() + async with self.patch_broker(broker) as br: + await br.start() + await br.publish_batch( + 1, + "hi", + 3, + topic=queue, + headers=Baggage({"foo": "bar"}).to_headers(), ) result, _ = await asyncio.wait( ( @@ -196,18 +200,20 @@ async def handler(msg, baggage: CurrentBaggage): async def test_single_publish_with_batch_consume( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, meter_provider: MeterProvider, metric_reader: InMemoryMetricReader, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + ) -> None: + event = asyncio.Event() + mid = self.telemetry_middleware_class( - meter_provider=meter_provider, tracer_provider=tracer_provider + meter_provider=meter_provider, + tracer_provider=tracer_provider, ) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) expected_msg_count = 2 expected_link_count = 2 expected_span_count = 6 @@ -217,27 +223,29 @@ async def test_single_publish_with_batch_consume( args, kwargs = self.get_subscriber_params(queue, batch=True) @broker.subscriber(*args, **kwargs) - async def handler(m, baggage: CurrentBaggage): + async def handler(m, baggage: CurrentBaggage) -> None: assert baggage.get_all() == expected_baggage assert len(baggage.get_all_batch()) == expected_msg_count m.sort() mock(m) event.set() - broker = self.patch_broker(broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() tasks = ( asyncio.create_task( - broker.publish( - "hi", topic=queue, headers=Baggage({"foo": "bar"}).to_headers() - ) + br.publish( + "hi", + topic=queue, + headers=Baggage({"foo": "bar"}).to_headers(), + ), ), asyncio.create_task( - broker.publish( - "buy", topic=queue, headers=Baggage({"bar": "baz"}).to_headers() - ) + br.publish( + "buy", + topic=queue, + headers=Baggage({"bar": "baz"}).to_headers(), + ), ), asyncio.create_task(event.wait()), ) @@ -259,9 +267,9 @@ async def handler(m, baggage: CurrentBaggage): mock.assert_called_once_with(["buy", "hi"]) -@pytest.mark.kafka +@pytest.mark.kafka() class TestPublishWithTelemetry(TestPublish): - def get_broker(self, apply_types: bool = False, **kwargs): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> KafkaBroker: return KafkaBroker( middlewares=(KafkaTelemetryMiddleware(),), apply_types=apply_types, @@ -269,9 +277,9 @@ def get_broker(self, apply_types: bool = False, **kwargs): ) -@pytest.mark.kafka +@pytest.mark.kafka() class TestConsumeWithTelemetry(TestConsume): - def get_broker(self, apply_types: bool = False, **kwargs): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> KafkaBroker: return KafkaBroker( middlewares=(KafkaTelemetryMiddleware(),), apply_types=apply_types, diff --git a/tests/opentelemetry/nats/test_nats.py b/tests/opentelemetry/nats/test_nats.py index d1262c7e6a..8c97bfb328 100644 --- a/tests/opentelemetry/nats/test_nats.py +++ b/tests/opentelemetry/nats/test_nats.py @@ -1,5 +1,6 @@ import asyncio -from unittest.mock import Mock +from typing import Any +from unittest.mock import MagicMock import pytest from opentelemetry.sdk.metrics import MeterProvider @@ -12,37 +13,40 @@ from faststream.nats.opentelemetry import NatsTelemetryMiddleware from tests.brokers.nats.test_consume import TestConsume from tests.brokers.nats.test_publish import TestPublish +from tests.opentelemetry.basic import LocalTelemetryTestcase -from ..basic import LocalTelemetryTestcase - -@pytest.fixture +@pytest.fixture() def stream(queue): return JStream(queue) -@pytest.mark.nats +@pytest.mark.nats() class TestTelemetry(LocalTelemetryTestcase): messaging_system = "nats" include_messages_counters = True - broker_class = NatsBroker telemetry_middleware_class = NatsTelemetryMiddleware + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> NatsBroker: + return NatsBroker(apply_types=apply_types, **kwargs) + async def test_batch( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, stream: JStream, meter_provider: MeterProvider, metric_reader: InMemoryMetricReader, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + ) -> None: + event = asyncio.Event() + mid = self.telemetry_middleware_class( - meter_provider=meter_provider, tracer_provider=tracer_provider + meter_provider=meter_provider, + tracer_provider=tracer_provider, ) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,)) expected_msg_count = 1 expected_span_count = 4 expected_proc_batch_count = 1 @@ -54,16 +58,14 @@ async def test_batch( ) @broker.subscriber(*args, **kwargs) - async def handler(m): + async def handler(m) -> None: mock(m) event.set() - broker = self.patch_broker(broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() tasks = ( - asyncio.create_task(broker.publish("hi", queue)), + asyncio.create_task(br.publish("hi", queue)), asyncio.create_task(event.wait()), ) await asyncio.wait(tasks, timeout=self.timeout) @@ -89,19 +91,21 @@ async def handler(m): mock.assert_called_once_with(["hi"]) -@pytest.mark.nats +@pytest.mark.nats() class TestPublishWithTelemetry(TestPublish): - def get_broker(self, apply_types: bool = False): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> NatsBroker: return NatsBroker( middlewares=(NatsTelemetryMiddleware(),), apply_types=apply_types, + **kwargs, ) -@pytest.mark.nats +@pytest.mark.nats() class TestConsumeWithTelemetry(TestConsume): - def get_broker(self, apply_types: bool = False): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> NatsBroker: return NatsBroker( middlewares=(NatsTelemetryMiddleware(),), apply_types=apply_types, + **kwargs, ) diff --git a/tests/opentelemetry/rabbit/test_rabbit.py b/tests/opentelemetry/rabbit/test_rabbit.py index 59d77c3b70..156e926416 100644 --- a/tests/opentelemetry/rabbit/test_rabbit.py +++ b/tests/opentelemetry/rabbit/test_rabbit.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any import pytest from dirty_equals import IsInt, IsUUID @@ -12,22 +12,23 @@ from faststream.rabbit.opentelemetry import RabbitTelemetryMiddleware from tests.brokers.rabbit.test_consume import TestConsume from tests.brokers.rabbit.test_publish import TestPublish +from tests.opentelemetry.basic import LocalTelemetryTestcase -from ..basic import LocalTelemetryTestcase - -@pytest.fixture +@pytest.fixture() def exchange(queue): return RabbitExchange(name=queue) -@pytest.mark.rabbit +@pytest.mark.rabbit() class TestTelemetry(LocalTelemetryTestcase): messaging_system = "rabbitmq" include_messages_counters = False - broker_class = RabbitBroker telemetry_middleware_class = RabbitTelemetryMiddleware + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RabbitBroker: + return RabbitBroker(apply_types=apply_types, **kwargs) + def destination_name(self, queue: str) -> str: return f"default.{queue}" @@ -37,19 +38,19 @@ def assert_span( action: str, queue: str, msg: str, - parent_span_id: Optional[str] = None, + parent_span_id: str | None = None, ) -> None: attrs = span.attributes assert attrs[SpanAttr.MESSAGING_SYSTEM] == self.messaging_system assert attrs[SpanAttr.MESSAGING_MESSAGE_CONVERSATION_ID] == IsUUID assert attrs[SpanAttr.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY] == queue assert span.name == f"{self.destination_name(queue)} {action}" - assert span.kind in (SpanKind.CONSUMER, SpanKind.PRODUCER) + assert span.kind in {SpanKind.CONSUMER, SpanKind.PRODUCER} - if span.kind == SpanKind.PRODUCER and action in (Action.CREATE, Action.PUBLISH): + if span.kind == SpanKind.PRODUCER and action in {Action.CREATE, Action.PUBLISH}: assert attrs[SpanAttr.MESSAGING_DESTINATION_NAME] == "" - if span.kind == SpanKind.CONSUMER and action in (Action.CREATE, Action.PROCESS): + if span.kind == SpanKind.CONSUMER and action in {Action.CREATE, Action.PROCESS}: assert attrs[MESSAGING_DESTINATION_PUBLISH_NAME] == "" assert attrs["messaging.rabbitmq.message.delivery_tag"] == IsInt assert attrs[SpanAttr.MESSAGING_MESSAGE_ID] == IsUUID @@ -65,19 +66,21 @@ def assert_span( assert span.parent.span_id == parent_span_id -@pytest.mark.rabbit +@pytest.mark.rabbit() class TestPublishWithTelemetry(TestPublish): - def get_broker(self, apply_types: bool = False): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RabbitBroker: return RabbitBroker( middlewares=(RabbitTelemetryMiddleware(),), apply_types=apply_types, + **kwargs, ) -@pytest.mark.rabbit +@pytest.mark.rabbit() class TestConsumeWithTelemetry(TestConsume): - def get_broker(self, apply_types: bool = False): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RabbitBroker: return RabbitBroker( middlewares=(RabbitTelemetryMiddleware(),), apply_types=apply_types, + **kwargs, ) diff --git a/tests/opentelemetry/redis/test_redis.py b/tests/opentelemetry/redis/test_redis.py index 9729b0a27f..95d1438703 100644 --- a/tests/opentelemetry/redis/test_redis.py +++ b/tests/opentelemetry/redis/test_redis.py @@ -1,5 +1,6 @@ import asyncio -from unittest.mock import Mock +from typing import Any +from unittest.mock import MagicMock import pytest from opentelemetry.sdk.metrics import MeterProvider @@ -17,31 +18,34 @@ TestConsumeStream, ) from tests.brokers.redis.test_publish import TestPublish +from tests.opentelemetry.basic import LocalTelemetryTestcase -from ..basic import LocalTelemetryTestcase - -@pytest.mark.redis +@pytest.mark.redis() class TestTelemetry(LocalTelemetryTestcase): messaging_system = "redis" include_messages_counters = True - broker_class = RedisBroker telemetry_middleware_class = RedisTelemetryMiddleware + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RedisBroker: + return RedisBroker(apply_types=apply_types, **kwargs) + async def test_batch( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, meter_provider: MeterProvider, metric_reader: InMemoryMetricReader, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + ) -> None: + event = asyncio.Event() + mid = self.telemetry_middleware_class( - meter_provider=meter_provider, tracer_provider=tracer_provider + meter_provider=meter_provider, + tracer_provider=tracer_provider, ) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) expected_msg_count = 3 expected_link_count = 1 expected_link_attrs = {"messaging.batch.message_count": 3} @@ -51,18 +55,16 @@ async def test_batch( args, kwargs = self.get_subscriber_params(list=ListSub(queue, batch=True)) @broker.subscriber(*args, **kwargs) - async def handler(m, baggage: CurrentBaggage): + async def handler(m, baggage: CurrentBaggage) -> None: assert baggage.get_all() == expected_baggage assert baggage.get_all_batch() == expected_baggage_batch mock(m) event.set() - broker = self.patch_broker(broker) - - async with broker: - await broker.start() + async with self.patch_broker(broker) as br: + await br.start() tasks = ( - asyncio.create_task(broker.publish_batch(1, "hi", 3, list=queue)), + asyncio.create_task(br.publish_batch(1, "hi", 3, list=queue)), asyncio.create_task(event.wait()), ) await asyncio.wait(tasks, timeout=self.timeout) @@ -93,11 +95,12 @@ async def test_batch_publish_with_single_consume( metric_reader: InMemoryMetricReader, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + ) -> None: mid = self.telemetry_middleware_class( - meter_provider=meter_provider, tracer_provider=tracer_provider + meter_provider=meter_provider, + tracer_provider=tracer_provider, ) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) msgs_queue = asyncio.Queue(maxsize=3) expected_msg_count = 3 expected_link_count = 1 @@ -109,16 +112,14 @@ async def test_batch_publish_with_single_consume( args, kwargs = self.get_subscriber_params(list=ListSub(queue)) @broker.subscriber(*args, **kwargs) - async def handler(msg, baggage: CurrentBaggage): + async def handler(msg, baggage: CurrentBaggage) -> None: assert baggage.get_all() == expected_baggage assert baggage.get_all_batch() == expected_baggage_batch await msgs_queue.put(msg) - broker = self.patch_broker(broker) - - async with broker: - await broker.start() - await broker.publish_batch(1, "hi", 3, list=queue) + async with self.patch_broker(broker) as br: + await br.start() + await br.publish_batch(1, "hi", 3, list=queue) result, _ = await asyncio.wait( ( asyncio.create_task(msgs_queue.get()), @@ -151,18 +152,20 @@ async def handler(msg, baggage: CurrentBaggage): async def test_single_publish_with_batch_consume( self, - event: asyncio.Event, queue: str, - mock: Mock, + mock: MagicMock, meter_provider: MeterProvider, metric_reader: InMemoryMetricReader, tracer_provider: TracerProvider, trace_exporter: InMemorySpanExporter, - ): + ) -> None: + event = asyncio.Event() + mid = self.telemetry_middleware_class( - meter_provider=meter_provider, tracer_provider=tracer_provider + meter_provider=meter_provider, + tracer_provider=tracer_provider, ) - broker = self.broker_class(middlewares=(mid,)) + broker = self.get_broker(middlewares=(mid,), apply_types=True) expected_msg_count = 2 expected_link_count = 2 expected_span_count = 6 @@ -172,32 +175,35 @@ async def test_single_publish_with_batch_consume( args, kwargs = self.get_subscriber_params(list=ListSub(queue, batch=True)) @broker.subscriber(*args, **kwargs) - async def handler(m, baggage: CurrentBaggage): + async def handler(m, baggage: CurrentBaggage) -> None: assert len(baggage.get_all_batch()) == expected_msg_count assert baggage.get_all() == expected_baggage m.sort() mock(m) event.set() - broker = self.patch_broker(broker) - - async with broker: + async with self.patch_broker(broker) as br: tasks = ( asyncio.create_task( - broker.publish( - "hi", list=queue, headers=Baggage({"foo": "bar"}).to_headers() - ) + br.publish( + "hi", + list=queue, + headers=Baggage({"foo": "bar"}).to_headers(), + ), ), asyncio.create_task( - broker.publish( - "buy", list=queue, headers=Baggage({"bar": "baz"}).to_headers() - ) + br.publish( + "buy", + list=queue, + headers=Baggage({"bar": "baz"}).to_headers(), + ), ), ) await asyncio.wait(tasks, timeout=self.timeout) await broker.start() await asyncio.wait( - (asyncio.create_task(event.wait()),), timeout=self.timeout + (asyncio.create_task(event.wait()),), + timeout=self.timeout, ) metrics = self.get_metrics(metric_reader) @@ -216,37 +222,41 @@ async def handler(m, baggage: CurrentBaggage): mock.assert_called_once_with(["buy", "hi"]) -@pytest.mark.redis +@pytest.mark.redis() class TestPublishWithTelemetry(TestPublish): - def get_broker(self, apply_types: bool = False): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RedisBroker: return RedisBroker( middlewares=(RedisTelemetryMiddleware(),), apply_types=apply_types, + **kwargs, ) -@pytest.mark.redis +@pytest.mark.redis() class TestConsumeWithTelemetry(TestConsume): - def get_broker(self, apply_types: bool = False): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RedisBroker: return RedisBroker( middlewares=(RedisTelemetryMiddleware(),), apply_types=apply_types, + **kwargs, ) -@pytest.mark.redis +@pytest.mark.redis() class TestConsumeListWithTelemetry(TestConsumeList): - def get_broker(self, apply_types: bool = False): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RedisBroker: return RedisBroker( middlewares=(RedisTelemetryMiddleware(),), apply_types=apply_types, + **kwargs, ) -@pytest.mark.redis +@pytest.mark.redis() class TestConsumeStreamWithTelemetry(TestConsumeStream): - def get_broker(self, apply_types: bool = False): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RedisBroker: return RedisBroker( middlewares=(RedisTelemetryMiddleware(),), apply_types=apply_types, + **kwargs, ) diff --git a/tests/prometheus/basic.py b/tests/prometheus/basic.py index 9f80a86b01..ac86500a1a 100644 --- a/tests/prometheus/basic.py +++ b/tests/prometheus/basic.py @@ -1,76 +1,89 @@ import asyncio -from typing import Any, Optional, Type -from unittest.mock import ANY, Mock, call +from typing import Any, cast import pytest +from dirty_equals import IsList, IsPositiveFloat, IsStr from prometheus_client import CollectorRegistry from faststream import Context -from faststream.broker.message import AckStatus from faststream.exceptions import IgnoredException, RejectMessage +from faststream.message import AckStatus +from faststream.prometheus import MetricsSettingsProvider from faststream.prometheus.middleware import ( PROCESSING_STATUS_BY_ACK_STATUS, PROCESSING_STATUS_BY_HANDLER_EXCEPTION_MAP, BasePrometheusMiddleware, ) -from faststream.prometheus.types import ProcessingStatus +from faststream.prometheus.types import ProcessingStatus, PublishingStatus from tests.brokers.base.basic import BaseTestcaseConfig +from tests.prometheus.utils import ( + get_published_messages_duration_seconds_metric, + get_published_messages_exceptions_metric, + get_published_messages_metric, + get_received_messages_in_process_metric, + get_received_messages_metric, + get_received_messages_size_bytes_metric, + get_received_processed_messages_duration_seconds_metric, + get_received_processed_messages_exceptions_metric, + get_received_processed_messages_metric, +) -@pytest.mark.asyncio +@pytest.mark.asyncio() class LocalPrometheusTestcase(BaseTestcaseConfig): - def get_broker(self, apply_types=False, **kwargs): + def get_middleware(self, **kwargs: Any) -> BasePrometheusMiddleware: raise NotImplementedError - def get_middleware(self, **kwargs) -> BasePrometheusMiddleware: + def get_settings_provider(self) -> MetricsSettingsProvider[Any]: raise NotImplementedError - @staticmethod - def consume_destination_name(queue: str) -> str: - return queue - - @property - def settings_provider_factory(self): - return self.get_middleware( - registry=CollectorRegistry() - )._settings_provider_factory - @pytest.mark.parametrize( ( "status", "exception_class", ), - [ + ( pytest.param( - AckStatus.acked, + AckStatus.ACKED, RejectMessage, id="acked status with reject message exception", ), pytest.param( - AckStatus.acked, Exception, id="acked status with not handler exception" + AckStatus.ACKED, + Exception, + id="acked status with not handler exception", + ), + pytest.param( + AckStatus.ACKED, + None, + id="acked status without exception", + ), + pytest.param( + AckStatus.NACKED, + None, + id="nacked status without exception", ), - pytest.param(AckStatus.acked, None, id="acked status without exception"), - pytest.param(AckStatus.nacked, None, id="nacked status without exception"), pytest.param( - AckStatus.rejected, None, id="rejected status without exception" + AckStatus.REJECTED, + None, + id="rejected status without exception", ), pytest.param( - AckStatus.acked, + AckStatus.ACKED, IgnoredException, id="acked status with ignore exception", ), - ], + ), ) async def test_metrics( self, - event: asyncio.Event, queue: str, status: AckStatus, - exception_class: Optional[Type[Exception]], - ): - middleware = self.get_middleware(registry=CollectorRegistry()) - metrics_manager_mock = Mock() - middleware._metrics_manager = metrics_manager_mock + exception_class: type[Exception] | None, + ) -> None: + event = asyncio.Event() + registry = CollectorRegistry() + middleware = self.get_middleware(registry=registry) broker = self.get_broker(apply_types=True, middlewares=(middleware,)) @@ -79,7 +92,7 @@ async def test_metrics( message = None @broker.subscriber(*args, **kwargs) - async def handler(m=Context("message")): + async def handler(m=Context("message")) -> None: event.set() nonlocal message @@ -88,11 +101,11 @@ async def handler(m=Context("message")): if exception_class: raise exception_class - if status == AckStatus.acked: + if status == AckStatus.ACKED: await message.ack() - elif status == AckStatus.nacked: + elif status == AckStatus.NACKED: await message.nack() - elif status == AckStatus.rejected: + elif status == AckStatus.REJECTED: await message.reject() async with broker: @@ -104,62 +117,69 @@ async def handler(m=Context("message")): await asyncio.wait(tasks, timeout=self.timeout) assert event.is_set() - self.assert_consume_metrics( - metrics_manager=metrics_manager_mock, + self.assert_metrics( + registry=registry, message=message, exception_class=exception_class, ) - self.assert_publish_metrics(metrics_manager=metrics_manager_mock) - def assert_consume_metrics( + def assert_metrics( self, *, - metrics_manager: Any, + registry: CollectorRegistry, message: Any, - exception_class: Optional[Type[Exception]], - ): - settings_provider = self.settings_provider_factory(message.raw_message) + exception_class: type[Exception] | None, + ) -> None: + settings_provider = self.get_settings_provider() consume_attrs = settings_provider.get_consume_attrs_from_message(message) - assert metrics_manager.add_received_message.mock_calls == [ - call( - amount=consume_attrs["messages_count"], - broker=settings_provider.messaging_system, - handler=consume_attrs["destination_name"], - ), - ] - assert metrics_manager.observe_received_messages_size.mock_calls == [ - call( - size=consume_attrs["message_size"], - broker=settings_provider.messaging_system, - handler=consume_attrs["destination_name"], - ), - ] + received_messages_metric = get_received_messages_metric( + metrics_prefix="faststream", + app_name="faststream", + broker=settings_provider.messaging_system, + queue=consume_attrs["destination_name"], + messages_amount=consume_attrs["messages_count"], + ) - assert metrics_manager.add_received_message_in_process.mock_calls == [ - call( - amount=consume_attrs["messages_count"], - broker=settings_provider.messaging_system, - handler=consume_attrs["destination_name"], + received_messages_size_bytes_metric = get_received_messages_size_bytes_metric( + metrics_prefix="faststream", + app_name="faststream", + broker=settings_provider.messaging_system, + queue=consume_attrs["destination_name"], + buckets=( + 2.0**4, + 2.0**6, + 2.0**8, + 2.0**10, + 2.0**12, + 2.0**14, + 2.0**16, + 2.0**18, + 2.0**20, + 2.0**22, + 2.0**24, + float("inf"), ), - ] - assert metrics_manager.remove_received_message_in_process.mock_calls == [ - call( - amount=consume_attrs["messages_count"], + size=consume_attrs["message_size"], + messages_amount=1, + ) + + received_messages_in_process_metric = get_received_messages_in_process_metric( + metrics_prefix="faststream", + app_name="faststream", + broker=settings_provider.messaging_system, + queue=consume_attrs["destination_name"], + messages_amount=0, + ) + + received_processed_messages_duration_seconds_metric = ( + get_received_processed_messages_duration_seconds_metric( + metrics_prefix="faststream", + app_name="faststream", broker=settings_provider.messaging_system, - handler=consume_attrs["destination_name"], + queue=consume_attrs["destination_name"], + duration=cast("float", IsPositiveFloat), ) - ] - - assert ( - metrics_manager.observe_received_processed_message_duration.mock_calls - == [ - call( - duration=ANY, - broker=settings_provider.messaging_system, - handler=consume_attrs["destination_name"], - ), - ] ) status = ProcessingStatus.acked @@ -172,51 +192,131 @@ def assert_consume_metrics( elif message.committed: status = PROCESSING_STATUS_BY_ACK_STATUS[message.committed] - assert metrics_manager.add_received_processed_message.mock_calls == [ - call( - amount=consume_attrs["messages_count"], - broker=settings_provider.messaging_system, - handler=consume_attrs["destination_name"], - status=status.value, - ), - ] + received_processed_messages_metric = get_received_processed_messages_metric( + metrics_prefix="faststream", + app_name="faststream", + broker=settings_provider.messaging_system, + queue=consume_attrs["destination_name"], + messages_amount=consume_attrs["messages_count"], + status=status, + ) + + exception_type: str | None = None if exception_class and not issubclass(exception_class, IgnoredException): - assert ( - metrics_manager.add_received_processed_message_exception.mock_calls - == [ - call( - broker=settings_provider.messaging_system, - handler=consume_attrs["destination_name"], - exception_type=exception_class.__name__, - ), - ] - ) - else: - assert ( - metrics_manager.add_received_processed_message_exception.mock_calls - == [] + exception_type = exception_class.__name__ + + received_processed_messages_exceptions_metric = ( + get_received_processed_messages_exceptions_metric( + metrics_prefix="faststream", + app_name="faststream", + broker=settings_provider.messaging_system, + queue=consume_attrs["destination_name"], + exception_type=exception_type, + exceptions_amount=consume_attrs["messages_count"], ) + ) - def assert_publish_metrics(self, metrics_manager: Any): - settings_provider = self.settings_provider_factory(None) - assert metrics_manager.observe_published_message_duration.mock_calls == [ - call( - duration=ANY, broker=settings_provider.messaging_system, destination=ANY - ), - ] - assert metrics_manager.add_published_message.mock_calls == [ - call( - amount=ANY, + published_messages_metric = get_published_messages_metric( + metrics_prefix="faststream", + app_name="faststream", + broker=settings_provider.messaging_system, + queue=cast("str", IsStr), + status=PublishingStatus.success, + messages_amount=consume_attrs["messages_count"], + ) + + published_messages_duration_seconds_metric = ( + get_published_messages_duration_seconds_metric( + metrics_prefix="faststream", + app_name="faststream", broker=settings_provider.messaging_system, - destination=ANY, - status="success", - ), - ] + queue=cast("str", IsStr), + duration=cast("float", IsPositiveFloat), + ) + ) + + published_messages_exceptions_metric = get_published_messages_exceptions_metric( + metrics_prefix="faststream", + app_name="faststream", + broker=settings_provider.messaging_system, + queue=cast("str", IsStr), + exception_type=None, + ) + + expected_metrics = IsList( + received_messages_metric, + received_messages_size_bytes_metric, + received_messages_in_process_metric, + received_processed_messages_metric, + received_processed_messages_duration_seconds_metric, + received_processed_messages_exceptions_metric, + published_messages_metric, + published_messages_duration_seconds_metric, + published_messages_exceptions_metric, + check_order=False, + ) + real_metrics = list(registry.collect()) - async def test_one_registry_for_some_middlewares( - self, event: asyncio.Event, queue: str + assert real_metrics == expected_metrics + + +class LocalRPCPrometheusTestcase: + @pytest.mark.asyncio() + async def test_rpc_request( + self, + queue: str, ) -> None: + event = asyncio.Event() + registry = CollectorRegistry() + + middleware = self.get_middleware(registry=registry) + + broker = self.get_broker(apply_types=True, middlewares=(middleware,)) + + message = None + + @broker.subscriber(queue) + async def handle(m=Context("message")): + event.set() + + nonlocal message + message = m + + return "" + + async with self.patch_broker(broker) as br: + await br.start() + + await asyncio.wait_for( + br.request("", queue), + timeout=3, + ) + + assert event.is_set() + + self.assert_metrics( + registry=registry, + message=message, + exception_class=None, + ) + + +class LocalMetricsSettingsProviderTestcase: + messaging_system: str + + def get_middleware(self, **kwargs) -> BasePrometheusMiddleware: + raise NotImplementedError + + @staticmethod + def get_settings_provider() -> MetricsSettingsProvider: + raise NotImplementedError + + def test_messaging_system(self) -> None: + provider = self.get_settings_provider() + assert provider.messaging_system == self.messaging_system + + def test_one_registry_for_some_middlewares(self) -> None: registry = CollectorRegistry() middleware_1 = self.get_middleware(registry=registry) diff --git a/tests/prometheus/confluent/basic.py b/tests/prometheus/confluent/basic.py new file mode 100644 index 0000000000..0852877951 --- /dev/null +++ b/tests/prometheus/confluent/basic.py @@ -0,0 +1,42 @@ +from typing import Any + +from faststream import AckPolicy +from faststream.confluent.prometheus import KafkaPrometheusMiddleware +from faststream.confluent.prometheus.provider import ( + BatchConfluentMetricsSettingsProvider, + ConfluentMetricsSettingsProvider, +) +from tests.brokers.confluent.basic import ConfluentTestcaseConfig + + +class BaseConfluentPrometheusSettings(ConfluentTestcaseConfig): + messaging_system = "kafka" + + def get_middleware(self, **kwargs: Any) -> KafkaPrometheusMiddleware: + return KafkaPrometheusMiddleware(**kwargs) + + def get_subscriber_params( + self, + *topics: Any, + **kwargs: Any, + ) -> tuple[ + tuple[Any, ...], + dict[str, Any], + ]: + topics, kwargs = super().get_subscriber_params(*topics, **kwargs) + + return topics, { + "group_id": "test", + "ack_policy": AckPolicy.REJECT_ON_ERROR, + **kwargs, + } + + +class ConfluentPrometheusSettings(BaseConfluentPrometheusSettings): + def get_settings_provider(self) -> ConfluentMetricsSettingsProvider: + return ConfluentMetricsSettingsProvider() + + +class BatchConfluentPrometheusSettings(BaseConfluentPrometheusSettings): + def get_settings_provider(self) -> BatchConfluentMetricsSettingsProvider: + return BatchConfluentMetricsSettingsProvider() diff --git a/tests/prometheus/confluent/test_confluent.py b/tests/prometheus/confluent/test_confluent.py index d1e3034ad6..fd745fc5b5 100644 --- a/tests/prometheus/confluent/test_confluent.py +++ b/tests/prometheus/confluent/test_confluent.py @@ -1,5 +1,5 @@ import asyncio -from unittest.mock import Mock +from typing import Any import pytest from prometheus_client import CollectorRegistry @@ -7,28 +7,18 @@ from faststream import Context from faststream.confluent import KafkaBroker from faststream.confluent.prometheus.middleware import KafkaPrometheusMiddleware -from tests.brokers.confluent.basic import ConfluentTestcaseConfig from tests.brokers.confluent.test_consume import TestConsume from tests.brokers.confluent.test_publish import TestPublish from tests.prometheus.basic import LocalPrometheusTestcase +from .basic import BatchConfluentPrometheusSettings, ConfluentPrometheusSettings -@pytest.mark.confluent -class TestPrometheus(ConfluentTestcaseConfig, LocalPrometheusTestcase): - def get_broker(self, apply_types=False, **kwargs): - return KafkaBroker(apply_types=apply_types, **kwargs) - def get_middleware(self, **kwargs): - return KafkaPrometheusMiddleware(**kwargs) - - async def test_metrics_batch( - self, - event: asyncio.Event, - queue: str, - ): - middleware = self.get_middleware(registry=CollectorRegistry()) - metrics_manager_mock = Mock() - middleware._metrics_manager = metrics_manager_mock +@pytest.mark.confluent() +class TestBatchPrometheus(BatchConfluentPrometheusSettings, LocalPrometheusTestcase): + async def test_metrics(self, queue: str, event: asyncio.Event) -> None: + registry = CollectorRegistry() + middleware = self.get_middleware(registry=registry) broker = self.get_broker(apply_types=True, middlewares=(middleware,)) @@ -36,7 +26,7 @@ async def test_metrics_batch( message = None @broker.subscriber(*args, **kwargs) - async def handler(m=Context("message")): + async def handler(m=Context("message")) -> None: event.set() nonlocal message @@ -53,15 +43,21 @@ async def handler(m=Context("message")): await asyncio.wait(tasks, timeout=self.timeout) assert event.is_set() - self.assert_consume_metrics( - metrics_manager=metrics_manager_mock, message=message, exception_class=None + self.assert_metrics( + registry=registry, + message=message, + exception_class=None, ) - self.assert_publish_metrics(metrics_manager=metrics_manager_mock) -@pytest.mark.confluent +@pytest.mark.confluent() +class TestPrometheus(ConfluentPrometheusSettings, LocalPrometheusTestcase): + pass + + +@pytest.mark.confluent() class TestPublishWithPrometheus(TestPublish): - def get_broker(self, apply_types: bool = False, **kwargs): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> KafkaBroker: return KafkaBroker( middlewares=(KafkaPrometheusMiddleware(registry=CollectorRegistry()),), apply_types=apply_types, @@ -69,9 +65,9 @@ def get_broker(self, apply_types: bool = False, **kwargs): ) -@pytest.mark.confluent +@pytest.mark.confluent() class TestConsumeWithPrometheus(TestConsume): - def get_broker(self, apply_types: bool = False, **kwargs): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> KafkaBroker: return KafkaBroker( middlewares=(KafkaPrometheusMiddleware(registry=CollectorRegistry()),), apply_types=apply_types, diff --git a/tests/prometheus/confluent/test_provider.py b/tests/prometheus/confluent/test_provider.py new file mode 100644 index 0000000000..a1099c3886 --- /dev/null +++ b/tests/prometheus/confluent/test_provider.py @@ -0,0 +1,97 @@ +import random +from types import SimpleNamespace + +import pytest + +from faststream.confluent.prometheus.provider import ( + BatchConfluentMetricsSettingsProvider, + ConfluentMetricsSettingsProvider, + settings_provider_factory, +) +from tests.prometheus.basic import LocalMetricsSettingsProviderTestcase + +from .basic import BatchConfluentPrometheusSettings, ConfluentPrometheusSettings + + +class LocalBaseConfluentMetricsSettingsProviderTestcase( + LocalMetricsSettingsProviderTestcase, +): + def test_get_publish_destination_name_from_cmd(self, queue: str) -> None: + expected_destination_name = queue + provider = self.get_settings_provider() + command = SimpleNamespace(destination=queue) + + destination_name = provider.get_publish_destination_name_from_cmd(command) + + assert destination_name == expected_destination_name + + +class TestKafkaMetricsSettingsProvider( + ConfluentPrometheusSettings, LocalBaseConfluentMetricsSettingsProviderTestcase +): + def test_get_consume_attrs_from_message(self, queue: str) -> None: + body = b"Hello" + expected_attrs = { + "destination_name": queue, + "message_size": len(body), + "messages_count": 1, + } + + message = SimpleNamespace( + body=body, raw_message=SimpleNamespace(topic=lambda: queue) + ) + + provider = self.get_settings_provider() + attrs = provider.get_consume_attrs_from_message(message) + + assert attrs == expected_attrs + + +class TestBatchConfluentMetricsSettingsProvider( + BatchConfluentPrometheusSettings, LocalBaseConfluentMetricsSettingsProviderTestcase +): + def test_get_consume_attrs_from_message(self, queue: str) -> None: + body = [b"Hi ", b"again, ", b"FastStream!"] + message = SimpleNamespace( + body=body, + raw_message=[ + SimpleNamespace(topic=lambda: queue) + for _ in range(random.randint(a=2, b=10)) + ], + ) + expected_attrs = { + "destination_name": message.raw_message[0].topic(), + "message_size": len(bytearray().join(body)), + "messages_count": len(message.raw_message), + } + + provider = self.get_settings_provider() + attrs = provider.get_consume_attrs_from_message(message) + + assert attrs == expected_attrs + + +@pytest.mark.parametrize( + ("msg", "expected_provider"), + ( + pytest.param( + (SimpleNamespace(), SimpleNamespace()), + BatchConfluentMetricsSettingsProvider(), + id="batch message", + ), + pytest.param( + SimpleNamespace(), + ConfluentMetricsSettingsProvider(), + id="single message", + ), + pytest.param( + None, + ConfluentMetricsSettingsProvider(), + id="None message", + ), + ), +) +def test_settings_provider_factory(msg, expected_provider) -> None: + provider = settings_provider_factory(msg) + + assert isinstance(provider, type(expected_provider)) diff --git a/tests/prometheus/kafka/basic.py b/tests/prometheus/kafka/basic.py new file mode 100644 index 0000000000..b4b2661037 --- /dev/null +++ b/tests/prometheus/kafka/basic.py @@ -0,0 +1,41 @@ +from typing import Any + +from faststream import AckPolicy +from faststream.kafka.prometheus import KafkaPrometheusMiddleware +from faststream.kafka.prometheus.provider import ( + BatchKafkaMetricsSettingsProvider, + KafkaMetricsSettingsProvider, +) +from tests.brokers.kafka.basic import KafkaTestcaseConfig + + +class BaseKafkaPrometheusSettings(KafkaTestcaseConfig): + messaging_system = "kafka" + + def get_middleware(self, **kwargs: Any) -> KafkaPrometheusMiddleware: + return KafkaPrometheusMiddleware(**kwargs) + + def get_subscriber_params( + self, + *args: Any, + **kwargs: Any, + ) -> tuple[ + tuple[Any, ...], + dict[str, Any], + ]: + args, kwargs = super().get_subscriber_params(*args, **kwargs) + return args, { + "group_id": "test", + "ack_policy": AckPolicy.REJECT_ON_ERROR, + **kwargs, + } + + +class KafkaPrometheusSettings(BaseKafkaPrometheusSettings): + def get_settings_provider(self) -> KafkaMetricsSettingsProvider: + return KafkaMetricsSettingsProvider() + + +class BatchKafkaPrometheusSettings(BaseKafkaPrometheusSettings): + def get_settings_provider(self) -> BatchKafkaMetricsSettingsProvider: + return BatchKafkaMetricsSettingsProvider() diff --git a/tests/prometheus/kafka/test_kafka.py b/tests/prometheus/kafka/test_kafka.py index abb5c86b3f..e2e0580b52 100644 --- a/tests/prometheus/kafka/test_kafka.py +++ b/tests/prometheus/kafka/test_kafka.py @@ -1,5 +1,4 @@ import asyncio -from unittest.mock import Mock import pytest from prometheus_client import CollectorRegistry @@ -11,23 +10,19 @@ from tests.brokers.kafka.test_publish import TestPublish from tests.prometheus.basic import LocalPrometheusTestcase +from .basic import BatchKafkaPrometheusSettings, KafkaPrometheusSettings -@pytest.mark.kafka -class TestPrometheus(LocalPrometheusTestcase): - def get_broker(self, apply_types=False, **kwargs): - return KafkaBroker(apply_types=apply_types, **kwargs) - def get_middleware(self, **kwargs): - return KafkaPrometheusMiddleware(**kwargs) - - async def test_metrics_batch( +@pytest.mark.kafka() +class TestBatchPrometheus(BatchKafkaPrometheusSettings, LocalPrometheusTestcase): + async def test_metrics( self, - event: asyncio.Event, queue: str, ): - middleware = self.get_middleware(registry=CollectorRegistry()) - metrics_manager_mock = Mock() - middleware._metrics_manager = metrics_manager_mock + event = asyncio.Event() + + registry = CollectorRegistry() + middleware = self.get_middleware(registry=registry) broker = self.get_broker(apply_types=True, middlewares=(middleware,)) @@ -52,13 +47,18 @@ async def handler(m=Context("message")): await asyncio.wait(tasks, timeout=self.timeout) assert event.is_set() - self.assert_consume_metrics( - metrics_manager=metrics_manager_mock, message=message, exception_class=None + self.assert_metrics( + registry=registry, + message=message, + exception_class=None, ) - self.assert_publish_metrics(metrics_manager=metrics_manager_mock) -@pytest.mark.kafka +@pytest.mark.kafka() +class TestPrometheus(KafkaPrometheusSettings, LocalPrometheusTestcase): ... + + +@pytest.mark.kafka() class TestPublishWithPrometheus(TestPublish): def get_broker( self, @@ -72,7 +72,7 @@ def get_broker( ) -@pytest.mark.kafka +@pytest.mark.kafka() class TestConsumeWithPrometheus(TestConsume): def get_broker(self, apply_types: bool = False, **kwargs): return KafkaBroker( diff --git a/tests/prometheus/kafka/test_provider.py b/tests/prometheus/kafka/test_provider.py new file mode 100644 index 0000000000..e046737f85 --- /dev/null +++ b/tests/prometheus/kafka/test_provider.py @@ -0,0 +1,96 @@ +import random +from types import SimpleNamespace + +import pytest + +from faststream.kafka.prometheus.provider import ( + BatchKafkaMetricsSettingsProvider, + KafkaMetricsSettingsProvider, + settings_provider_factory, +) +from tests.prometheus.basic import LocalMetricsSettingsProviderTestcase + +from .basic import BatchKafkaPrometheusSettings, KafkaPrometheusSettings + + +class LocalBaseKafkaMetricsSettingsProviderTestcase( + LocalMetricsSettingsProviderTestcase, +): + def test_get_publish_destination_name_from_cmd(self, queue: str) -> None: + expected_destination_name = queue + provider = self.get_settings_provider() + command = SimpleNamespace(destination=queue) + + destination_name = provider.get_publish_destination_name_from_cmd(command) + + assert destination_name == expected_destination_name + + +class TestKafkaMetricsSettingsProvider( + KafkaPrometheusSettings, + LocalBaseKafkaMetricsSettingsProviderTestcase, +): + def test_get_consume_attrs_from_message(self, queue: str) -> None: + body = b"Hello" + expected_attrs = { + "destination_name": queue, + "message_size": len(body), + "messages_count": 1, + } + + message = SimpleNamespace(body=body, raw_message=SimpleNamespace(topic=queue)) + + provider = self.get_settings_provider() + attrs = provider.get_consume_attrs_from_message(message) + + assert attrs == expected_attrs + + +class TestBatchKafkaMetricsSettingsProvider( + BatchKafkaPrometheusSettings, + LocalBaseKafkaMetricsSettingsProviderTestcase, +): + def test_get_consume_attrs_from_message(self, queue: str) -> None: + body = [b"Hi ", b"again, ", b"FastStream!"] + message = SimpleNamespace( + body=body, + raw_message=[ + SimpleNamespace(topic=queue) for _ in range(random.randint(a=2, b=10)) + ], + ) + expected_attrs = { + "destination_name": message.raw_message[0].topic, + "message_size": len(bytearray().join(body)), + "messages_count": len(message.raw_message), + } + + provider = self.get_settings_provider() + attrs = provider.get_consume_attrs_from_message(message) + + assert attrs == expected_attrs + + +@pytest.mark.parametrize( + ("msg", "expected_provider"), + ( + pytest.param( + (SimpleNamespace(), SimpleNamespace()), + BatchKafkaMetricsSettingsProvider(), + id="batch message", + ), + pytest.param( + SimpleNamespace(), + KafkaMetricsSettingsProvider(), + id="single message", + ), + pytest.param( + None, + KafkaMetricsSettingsProvider(), + id="None message", + ), + ), +) +def test_settings_provider_factory(msg, expected_provider) -> None: + provider = settings_provider_factory(msg) + + assert isinstance(provider, type(expected_provider)) diff --git a/tests/prometheus/nats/basic.py b/tests/prometheus/nats/basic.py new file mode 100644 index 0000000000..4fda6435e7 --- /dev/null +++ b/tests/prometheus/nats/basic.py @@ -0,0 +1,25 @@ +from typing import Any + +from faststream.nats.prometheus import NatsPrometheusMiddleware +from faststream.nats.prometheus.provider import ( + BatchNatsMetricsSettingsProvider, + NatsMetricsSettingsProvider, +) +from tests.brokers.nats.basic import NatsTestcaseConfig + + +class BaseNatsPrometheusSettings(NatsTestcaseConfig): + messaging_system = "nats" + + def get_middleware(self, **kwargs: Any) -> NatsPrometheusMiddleware: + return NatsPrometheusMiddleware(**kwargs) + + +class NatsPrometheusSettings(BaseNatsPrometheusSettings): + def get_settings_provider(self) -> NatsMetricsSettingsProvider: + return NatsMetricsSettingsProvider() + + +class BatchNatsPrometheusSettings(BaseNatsPrometheusSettings): + def get_settings_provider(self) -> BatchNatsMetricsSettingsProvider: + return BatchNatsMetricsSettingsProvider() diff --git a/tests/prometheus/nats/test_nats.py b/tests/prometheus/nats/test_nats.py index f65eb41e85..b9ea9c89a2 100644 --- a/tests/prometheus/nats/test_nats.py +++ b/tests/prometheus/nats/test_nats.py @@ -1,5 +1,5 @@ import asyncio -from unittest.mock import Mock +from typing import Any import pytest from prometheus_client import CollectorRegistry @@ -9,31 +9,27 @@ from faststream.nats.prometheus.middleware import NatsPrometheusMiddleware from tests.brokers.nats.test_consume import TestConsume from tests.brokers.nats.test_publish import TestPublish -from tests.prometheus.basic import LocalPrometheusTestcase +from tests.prometheus.basic import LocalPrometheusTestcase, LocalRPCPrometheusTestcase +from .basic import BatchNatsPrometheusSettings, NatsPrometheusSettings -@pytest.fixture + +@pytest.fixture() def stream(queue): return JStream(queue) -@pytest.mark.nats -class TestPrometheus(LocalPrometheusTestcase): - def get_broker(self, apply_types=False, **kwargs): - return NatsBroker(apply_types=apply_types, **kwargs) - - def get_middleware(self, **kwargs): - return NatsPrometheusMiddleware(**kwargs) - - async def test_metrics_batch( +@pytest.mark.nats() +class TestBatchPrometheus(BatchNatsPrometheusSettings, LocalPrometheusTestcase): + async def test_metrics( self, - event: asyncio.Event, queue: str, stream: JStream, - ): - middleware = self.get_middleware(registry=CollectorRegistry()) - metrics_manager_mock = Mock() - middleware._metrics_manager = metrics_manager_mock + ) -> None: + event = asyncio.Event() + + registry = CollectorRegistry() + middleware = self.get_middleware(registry=registry) broker = self.get_broker(apply_types=True, middlewares=(middleware,)) @@ -60,15 +56,24 @@ async def handler(m=Context("message")): await asyncio.wait(tasks, timeout=self.timeout) assert event.is_set() - self.assert_consume_metrics( - metrics_manager=metrics_manager_mock, message=message, exception_class=None + self.assert_metrics( + registry=registry, + message=message, + exception_class=None, ) - self.assert_publish_metrics(metrics_manager=metrics_manager_mock) -@pytest.mark.nats +@pytest.mark.nats() +class TestPrometheus( + NatsPrometheusSettings, + LocalPrometheusTestcase, + LocalRPCPrometheusTestcase, +): ... + + +@pytest.mark.nats() class TestPublishWithPrometheus(TestPublish): - def get_broker(self, apply_types: bool = False, **kwargs): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> NatsBroker: return NatsBroker( middlewares=(NatsPrometheusMiddleware(registry=CollectorRegistry()),), apply_types=apply_types, @@ -76,9 +81,9 @@ def get_broker(self, apply_types: bool = False, **kwargs): ) -@pytest.mark.nats +@pytest.mark.nats() class TestConsumeWithPrometheus(TestConsume): - def get_broker(self, apply_types: bool = False, **kwargs): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> NatsBroker: return NatsBroker( middlewares=(NatsPrometheusMiddleware(registry=CollectorRegistry()),), apply_types=apply_types, diff --git a/tests/prometheus/nats/test_provider.py b/tests/prometheus/nats/test_provider.py new file mode 100644 index 0000000000..3c94a7e129 --- /dev/null +++ b/tests/prometheus/nats/test_provider.py @@ -0,0 +1,100 @@ +import random +from types import SimpleNamespace + +import pytest +from nats.aio.msg import Msg + +from faststream.nats.prometheus.provider import ( + BatchNatsMetricsSettingsProvider, + NatsMetricsSettingsProvider, + settings_provider_factory, +) +from tests.prometheus.basic import LocalMetricsSettingsProviderTestcase + +from .basic import BatchNatsPrometheusSettings, NatsPrometheusSettings + + +class LocalBaseNatsMetricsSettingsProviderTestcase( + LocalMetricsSettingsProviderTestcase +): + def test_get_publish_destination_name_from_cmd(self, queue: str) -> None: + expected_destination_name = queue + command = SimpleNamespace(destination=queue) + + provider = self.get_settings_provider() + destination_name = provider.get_publish_destination_name_from_cmd(command) + + assert destination_name == expected_destination_name + + +class TestNatsMetricsSettingsProvider( + NatsPrometheusSettings, LocalBaseNatsMetricsSettingsProviderTestcase +): + def test_get_consume_attrs_from_message(self, queue: str) -> None: + body = b"Hello" + expected_attrs = { + "destination_name": queue, + "message_size": len(body), + "messages_count": 1, + } + message = SimpleNamespace(body=body, raw_message=SimpleNamespace(subject=queue)) + + provider = self.get_settings_provider() + attrs = provider.get_consume_attrs_from_message(message) + + assert attrs == expected_attrs + + +class TestBatchNatsMetricsSettingsProvider( + BatchNatsPrometheusSettings, LocalBaseNatsMetricsSettingsProviderTestcase +): + def test_get_consume_attrs_from_message(self, queue: str) -> None: + body = b"Hello" + raw_messages = [ + SimpleNamespace(subject=queue) for _ in range(random.randint(a=2, b=10)) + ] + + expected_attrs = { + "destination_name": raw_messages[0].subject, + "message_size": len(body), + "messages_count": len(raw_messages), + } + message = SimpleNamespace(body=body, raw_message=raw_messages) + + provider = self.get_settings_provider() + attrs = provider.get_consume_attrs_from_message(message) + + assert attrs == expected_attrs + + +@pytest.mark.parametrize( + ("msg", "expected_provider"), + ( + pytest.param( + (Msg(SimpleNamespace()), Msg(SimpleNamespace())), + BatchNatsMetricsSettingsProvider(), + id="message is sequence", + ), + pytest.param( + Msg( + SimpleNamespace(), + ), + NatsMetricsSettingsProvider(), + id="single message", + ), + pytest.param( + None, + NatsMetricsSettingsProvider(), + id="message is None", + ), + pytest.param( + SimpleNamespace(), + None, + id="message is not Msg instance", + ), + ), +) +def test_settings_provider_factory(msg, expected_provider) -> None: + provider = settings_provider_factory(msg) + + assert isinstance(provider, type(expected_provider)) diff --git a/tests/prometheus/rabbit/basic.py b/tests/prometheus/rabbit/basic.py new file mode 100644 index 0000000000..ee8a2d20a9 --- /dev/null +++ b/tests/prometheus/rabbit/basic.py @@ -0,0 +1,16 @@ +from typing import Any + +from faststream.prometheus import MetricsSettingsProvider +from faststream.rabbit.prometheus import RabbitPrometheusMiddleware +from faststream.rabbit.prometheus.provider import RabbitMetricsSettingsProvider +from tests.brokers.rabbit.basic import RabbitTestcaseConfig + + +class RabbitPrometheusSettings(RabbitTestcaseConfig): + messaging_system = "rabbitmq" + + def get_middleware(self, **kwargs: Any) -> RabbitPrometheusMiddleware: + return RabbitPrometheusMiddleware(**kwargs) + + def get_settings_provider(self) -> MetricsSettingsProvider[Any]: + return RabbitMetricsSettingsProvider() diff --git a/tests/prometheus/rabbit/test_provider.py b/tests/prometheus/rabbit/test_provider.py new file mode 100644 index 0000000000..3f4805df0d --- /dev/null +++ b/tests/prometheus/rabbit/test_provider.py @@ -0,0 +1,61 @@ +from types import SimpleNamespace + +import pytest + +from tests.prometheus.basic import LocalMetricsSettingsProviderTestcase + +from .basic import RabbitPrometheusSettings + + +class TestRabbitMetricsSettingsProvider( + RabbitPrometheusSettings, + LocalMetricsSettingsProviderTestcase, +): + @pytest.mark.parametrize( + "exchange", + ( + pytest.param("my_exchange", id="with exchange"), + pytest.param(None, id="without exchange"), + ), + ) + def test_get_consume_attrs_from_message( + self, + exchange: str | None, + queue: str, + ) -> None: + body = b"Hello" + expected_attrs = { + "destination_name": f"{exchange or 'default'}.{queue}", + "message_size": len(body), + "messages_count": 1, + } + message = SimpleNamespace( + body=body, raw_message=SimpleNamespace(exchange=exchange, routing_key=queue) + ) + + provider = self.get_settings_provider() + attrs = provider.get_consume_attrs_from_message(message) + + assert attrs == expected_attrs + + @pytest.mark.parametrize( + "exchange", + ( + pytest.param("my_exchange", id="with exchange"), + pytest.param(None, id="without exchange"), + ), + ) + def test_get_publish_destination_name_from_cmd( + self, + exchange: str | None, + queue: str, + ) -> None: + expected_destination_name = f"{exchange or 'default'}.{queue}" + command = SimpleNamespace( + exchange=SimpleNamespace(name=exchange), destination=queue + ) + + provider = self.get_settings_provider() + destination_name = provider.get_publish_destination_name_from_cmd(command) + + assert destination_name == expected_destination_name diff --git a/tests/prometheus/rabbit/test_rabbit.py b/tests/prometheus/rabbit/test_rabbit.py index 6eef6d224f..dff264063a 100644 --- a/tests/prometheus/rabbit/test_rabbit.py +++ b/tests/prometheus/rabbit/test_rabbit.py @@ -1,3 +1,5 @@ +from typing import Any + import pytest from prometheus_client import CollectorRegistry @@ -5,26 +7,26 @@ from faststream.rabbit.prometheus.middleware import RabbitPrometheusMiddleware from tests.brokers.rabbit.test_consume import TestConsume from tests.brokers.rabbit.test_publish import TestPublish -from tests.prometheus.basic import LocalPrometheusTestcase +from tests.prometheus.basic import LocalPrometheusTestcase, LocalRPCPrometheusTestcase + +from .basic import RabbitPrometheusSettings -@pytest.fixture +@pytest.fixture() def exchange(queue): return RabbitExchange(name=queue) -@pytest.mark.rabbit -class TestPrometheus(LocalPrometheusTestcase): - def get_broker(self, apply_types=False, **kwargs): - return RabbitBroker(apply_types=apply_types, **kwargs) - - def get_middleware(self, **kwargs): - return RabbitPrometheusMiddleware(**kwargs) +@pytest.mark.rabbit() +class TestPrometheus( + RabbitPrometheusSettings, LocalPrometheusTestcase, LocalRPCPrometheusTestcase +): + pass -@pytest.mark.rabbit +@pytest.mark.rabbit() class TestPublishWithPrometheus(TestPublish): - def get_broker(self, apply_types: bool = False, **kwargs): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RabbitBroker: return RabbitBroker( middlewares=(RabbitPrometheusMiddleware(registry=CollectorRegistry()),), apply_types=apply_types, @@ -32,9 +34,9 @@ def get_broker(self, apply_types: bool = False, **kwargs): ) -@pytest.mark.rabbit +@pytest.mark.rabbit() class TestConsumeWithPrometheus(TestConsume): - def get_broker(self, apply_types: bool = False, **kwargs): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RabbitBroker: return RabbitBroker( middlewares=(RabbitPrometheusMiddleware(registry=CollectorRegistry()),), apply_types=apply_types, diff --git a/tests/prometheus/redis/basic.py b/tests/prometheus/redis/basic.py new file mode 100644 index 0000000000..79e3bb477b --- /dev/null +++ b/tests/prometheus/redis/basic.py @@ -0,0 +1,25 @@ +from typing import Any + +from faststream.redis.prometheus import RedisPrometheusMiddleware +from faststream.redis.prometheus.provider import ( + BatchRedisMetricsSettingsProvider, + RedisMetricsSettingsProvider, +) +from tests.brokers.redis.basic import RedisTestcaseConfig + + +class BaseRedisPrometheusSettings(RedisTestcaseConfig): + messaging_system = "redis" + + def get_middleware(self, **kwargs: Any) -> RedisPrometheusMiddleware: + return RedisPrometheusMiddleware(**kwargs) + + +class RedisPrometheusSettings(BaseRedisPrometheusSettings): + def get_settings_provider(self) -> RedisMetricsSettingsProvider: + return RedisMetricsSettingsProvider() + + +class BatchRedisPrometheusSettings(BaseRedisPrometheusSettings): + def get_settings_provider(self) -> BatchRedisMetricsSettingsProvider: + return BatchRedisMetricsSettingsProvider() diff --git a/tests/prometheus/redis/test_provider.py b/tests/prometheus/redis/test_provider.py new file mode 100644 index 0000000000..1e4fef4581 --- /dev/null +++ b/tests/prometheus/redis/test_provider.py @@ -0,0 +1,154 @@ +from types import SimpleNamespace + +import pytest + +from faststream.redis.message import ( + BatchListMessage, + BatchStreamMessage, + DefaultListMessage, + DefaultStreamMessage, + PubSubMessage, +) +from faststream.redis.prometheus.provider import ( + BatchRedisMetricsSettingsProvider, + RedisMetricsSettingsProvider, + settings_provider_factory, +) +from tests.prometheus.basic import LocalMetricsSettingsProviderTestcase + +from .basic import BatchRedisPrometheusSettings, RedisPrometheusSettings + + +class LocalBaseRedisMetricsSettingsProviderTestcase( + LocalMetricsSettingsProviderTestcase +): + def test_get_publish_destination_name_from_cmd(self, queue: str) -> None: + expected_destination_name = queue + provider = self.get_settings_provider() + command = SimpleNamespace(destination=queue) + + destination_name = provider.get_publish_destination_name_from_cmd(command) + + assert destination_name == expected_destination_name + + +class TestRedisMetricsSettingsProvider( + RedisPrometheusSettings, LocalBaseRedisMetricsSettingsProviderTestcase +): + @pytest.mark.parametrize( + "destination", + ( + pytest.param("channel", id="destination is channel"), + pytest.param("list", id="destination is list"), + pytest.param("stream", id="destination is stream"), + pytest.param("", id="destination is blank"), + ), + ) + def test_get_consume_attrs_from_message(self, queue: str, destination: str) -> None: + body = b"Hello" + expected_attrs = { + "destination_name": queue if destination else "", + "message_size": len(body), + "messages_count": 1, + } + + raw_message = {"data": body} + if destination: + raw_message[destination] = queue + + message = SimpleNamespace(body=body, raw_message=raw_message) + + provider = self.get_settings_provider() + attrs = provider.get_consume_attrs_from_message(message) + + assert attrs == expected_attrs + + +class TestBatchRedisMetricsSettingsProvider( + BatchRedisPrometheusSettings, + LocalBaseRedisMetricsSettingsProviderTestcase, +): + @pytest.mark.parametrize( + "destination", + ( + pytest.param("channel", id="destination is channel"), + pytest.param("list", id="destination is list"), + pytest.param("stream", id="destination is stream"), + pytest.param("", id="destination is blank"), + ), + ) + def test_get_consume_attrs_from_message(self, queue: str, destination: str) -> None: + decoded_body = ["Hi ", "again, ", "FastStream!"] + body = str(decoded_body).encode() + + expected_attrs = { + "destination_name": queue if destination else "", + "message_size": len(body), + "messages_count": len(decoded_body), + } + + raw_message = {"data": decoded_body} + + if destination: + raw_message[destination] = queue + + message = SimpleNamespace( + body=body, + raw_message=raw_message, + ) + + provider = self.get_settings_provider() + attrs = provider.get_consume_attrs_from_message(message) + + assert attrs == expected_attrs + + +@pytest.mark.parametrize( + ("msg", "expected_provider"), + ( + pytest.param( + PubSubMessage( + type="message", + channel="test-channel", + data=b"", + pattern=None, + ), + RedisMetricsSettingsProvider(), + id="PubSub message", + ), + pytest.param( + DefaultListMessage(type="list", channel="test-list", data=b""), + RedisMetricsSettingsProvider(), + id="Single List message", + ), + pytest.param( + BatchListMessage(type="blist", channel="test-list", data=[b"", b""]), + BatchRedisMetricsSettingsProvider(), + id="Batch List message", + ), + pytest.param( + DefaultStreamMessage( + type="stream", + channel="test-stream", + data=b"", + message_ids=[], + ), + RedisMetricsSettingsProvider(), + id="Single Stream message", + ), + pytest.param( + BatchStreamMessage( + type="bstream", + channel="test-stream", + data=[{b"": b""}, {b"": b""}], + message_ids=[], + ), + BatchRedisMetricsSettingsProvider(), + id="Batch Stream message", + ), + ), +) +def test_settings_provider_factory(msg, expected_provider) -> None: + provider = settings_provider_factory(msg) + + assert isinstance(provider, type(expected_provider)) diff --git a/tests/prometheus/redis/test_redis.py b/tests/prometheus/redis/test_redis.py index 4059c33d48..fae4932bec 100644 --- a/tests/prometheus/redis/test_redis.py +++ b/tests/prometheus/redis/test_redis.py @@ -1,5 +1,5 @@ import asyncio -from unittest.mock import Mock +from typing import Any import pytest from prometheus_client import CollectorRegistry @@ -9,25 +9,21 @@ from faststream.redis.prometheus.middleware import RedisPrometheusMiddleware from tests.brokers.redis.test_consume import TestConsume from tests.brokers.redis.test_publish import TestPublish -from tests.prometheus.basic import LocalPrometheusTestcase +from tests.prometheus.basic import LocalPrometheusTestcase, LocalRPCPrometheusTestcase +from .basic import BatchRedisPrometheusSettings, RedisPrometheusSettings -@pytest.mark.redis -class TestPrometheus(LocalPrometheusTestcase): - def get_broker(self, apply_types=False, **kwargs): - return RedisBroker(apply_types=apply_types, **kwargs) - def get_middleware(self, **kwargs): - return RedisPrometheusMiddleware(**kwargs) - - async def test_metrics_batch( +@pytest.mark.redis() +class TestBatchPrometheus(BatchRedisPrometheusSettings, LocalPrometheusTestcase): + async def test_metrics( self, - event: asyncio.Event, queue: str, - ): - middleware = self.get_middleware(registry=CollectorRegistry()) - metrics_manager_mock = Mock() - middleware._metrics_manager = metrics_manager_mock + ) -> None: + event = asyncio.Event() + + registry = CollectorRegistry() + middleware = self.get_middleware(registry=registry) broker = self.get_broker(apply_types=True, middlewares=(middleware,)) @@ -45,21 +41,30 @@ async def handler(m=Context("message")): async with broker: await broker.start() tasks = ( - asyncio.create_task(broker.publish_batch("hello", "world", list=queue)), + asyncio.create_task(broker.publish_batch(1, 2, list=queue)), asyncio.create_task(event.wait()), ) await asyncio.wait(tasks, timeout=self.timeout) assert event.is_set() - self.assert_consume_metrics( - metrics_manager=metrics_manager_mock, message=message, exception_class=None + self.assert_metrics( + registry=registry, + message=message, + exception_class=None, ) - self.assert_publish_metrics(metrics_manager=metrics_manager_mock) -@pytest.mark.redis +@pytest.mark.redis() +class TestPrometheus( + RedisPrometheusSettings, + LocalPrometheusTestcase, + LocalRPCPrometheusTestcase, +): ... + + +@pytest.mark.redis() class TestPublishWithPrometheus(TestPublish): - def get_broker(self, apply_types: bool = False, **kwargs): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RedisBroker: return RedisBroker( middlewares=(RedisPrometheusMiddleware(registry=CollectorRegistry()),), apply_types=apply_types, @@ -67,9 +72,9 @@ def get_broker(self, apply_types: bool = False, **kwargs): ) -@pytest.mark.redis +@pytest.mark.redis() class TestConsumeWithPrometheus(TestConsume): - def get_broker(self, apply_types: bool = False, **kwargs): + def get_broker(self, apply_types: bool = False, **kwargs: Any) -> RedisBroker: return RedisBroker( middlewares=(RedisPrometheusMiddleware(registry=CollectorRegistry()),), apply_types=apply_types, diff --git a/tests/prometheus/test_metrics.py b/tests/prometheus/test_metrics.py index 7f9aa85771..dae8e1ed14 100644 --- a/tests/prometheus/test_metrics.py +++ b/tests/prometheus/test_metrics.py @@ -1,23 +1,31 @@ import random -from typing import List, Optional -from unittest.mock import ANY +from typing import Any import pytest -from dirty_equals import IsPositiveFloat, IsStr -from prometheus_client import CollectorRegistry, Histogram, Metric -from prometheus_client.samples import Sample +from prometheus_client import CollectorRegistry from faststream.prometheus.container import MetricsContainer from faststream.prometheus.manager import MetricsManager from faststream.prometheus.types import ProcessingStatus, PublishingStatus +from tests.prometheus.utils import ( + get_published_messages_duration_seconds_metric, + get_published_messages_exceptions_metric, + get_published_messages_metric, + get_received_messages_in_process_metric, + get_received_messages_metric, + get_received_messages_size_bytes_metric, + get_received_processed_messages_duration_seconds_metric, + get_received_processed_messages_exceptions_metric, + get_received_processed_messages_metric, +) class TestCaseMetrics: @staticmethod def create_metrics_manager( - app_name: Optional[str] = None, - metrics_prefix: Optional[str] = None, - received_messages_size_buckets: Optional[List[float]] = None, + app_name: str, + metrics_prefix: str, + received_messages_size_buckets: list[float] | None = None, ) -> MetricsManager: registry = CollectorRegistry() container = MetricsContainer( @@ -27,27 +35,27 @@ def create_metrics_manager( ) return MetricsManager(container, app_name=app_name) - @pytest.fixture + @pytest.fixture() def app_name(self, request) -> str: return "youtube" - @pytest.fixture + @pytest.fixture() def metrics_prefix(self, request) -> str: return "fs" - @pytest.fixture + @pytest.fixture() def broker(self) -> str: return "rabbit" - @pytest.fixture + @pytest.fixture() def queue(self) -> str: return "default.test" - @pytest.fixture + @pytest.fixture() def messages_amount(self) -> int: return random.randint(1, 10) - @pytest.fixture + @pytest.fixture() def exception_type(self) -> str: return Exception.__name__ @@ -64,28 +72,13 @@ def test_add_received_message( metrics_prefix=metrics_prefix, ) - expected = Metric( - name=f"{metrics_prefix}_received_messages", - documentation="Count of received messages by broker and handler", - unit="", - typ="counter", - ) - expected.samples = [ - Sample( - name=f"{metrics_prefix}_received_messages_total", - labels={"app_name": app_name, "broker": broker, "handler": queue}, - value=float(messages_amount), - timestamp=None, - exemplar=None, - ), - Sample( - name=f"{metrics_prefix}_received_messages_created", - labels={"app_name": app_name, "broker": broker, "handler": queue}, - value=IsPositiveFloat, - timestamp=None, - exemplar=None, - ), - ] + expected = get_received_messages_metric( + app_name=app_name, + metrics_prefix=metrics_prefix, + queue=queue, + broker=broker, + messages_amount=messages_amount, + ) manager.add_received_message( amount=messages_amount, broker=broker, handler=queue @@ -97,10 +90,10 @@ def test_add_received_message( @pytest.mark.parametrize( "is_default_buckets", - [ + ( pytest.param(True, id="with default buckets"), pytest.param(False, id="with custom buckets"), - ], + ), ) def test_observe_received_messages_size( self, @@ -110,7 +103,7 @@ def test_observe_received_messages_size( broker: str, is_default_buckets: bool, ) -> None: - manager_kwargs = { + manager_kwargs: dict[str, Any] = { "app_name": app_name, "metrics_prefix": metrics_prefix, } @@ -129,50 +122,15 @@ def test_observe_received_messages_size( else custom_buckets ) - expected = Metric( - name=f"{metrics_prefix}_received_messages_size_bytes", - documentation="Histogram of received messages size in bytes by broker and handler", - unit="", - typ="histogram", - ) - expected.samples = [ - *[ - Sample( - name=f"{metrics_prefix}_received_messages_size_bytes_bucket", - labels={ - "app_name": app_name, - "broker": broker, - "handler": queue, - "le": IsStr, - }, - value=1.0, - timestamp=None, - exemplar=None, - ) - for _ in buckets - ], - Sample( - name=f"{metrics_prefix}_received_messages_size_bytes_count", - labels={"app_name": app_name, "broker": broker, "handler": queue}, - value=1.0, - timestamp=None, - exemplar=None, - ), - Sample( - name=f"{metrics_prefix}_received_messages_size_bytes_sum", - labels={"app_name": app_name, "broker": broker, "handler": queue}, - value=size, - timestamp=None, - exemplar=None, - ), - Sample( - name=f"{metrics_prefix}_received_messages_size_bytes_created", - labels={"app_name": app_name, "broker": broker, "handler": queue}, - value=ANY, - timestamp=None, - exemplar=None, - ), - ] + expected = get_received_messages_size_bytes_metric( + metrics_prefix=metrics_prefix, + app_name=app_name, + broker=broker, + queue=queue, + buckets=buckets, + size=size, + messages_amount=1, + ) manager.observe_received_messages_size(size=size, broker=broker, handler=queue) @@ -193,21 +151,13 @@ def test_add_received_message_in_process( metrics_prefix=metrics_prefix, ) - expected = Metric( - name=f"{metrics_prefix}_received_messages_in_process", - documentation="Gauge of received messages in process by broker and handler", - unit="", - typ="gauge", + expected = get_received_messages_in_process_metric( + metrics_prefix=metrics_prefix, + app_name=app_name, + broker=broker, + queue=queue, + messages_amount=messages_amount, ) - expected.samples = [ - Sample( - name=f"{metrics_prefix}_received_messages_in_process", - labels={"app_name": app_name, "broker": broker, "handler": queue}, - value=float(messages_amount), - timestamp=None, - exemplar=None, - ), - ] manager.add_received_message_in_process( amount=messages_amount, broker=broker, handler=queue @@ -230,21 +180,13 @@ def test_remove_received_message_in_process( metrics_prefix=metrics_prefix, ) - expected = Metric( - name=f"{metrics_prefix}_received_messages_in_process", - documentation="Gauge of received messages in process by broker and handler", - unit="", - typ="gauge", + expected = get_received_messages_in_process_metric( + metrics_prefix=metrics_prefix, + app_name=app_name, + broker=broker, + queue=queue, + messages_amount=messages_amount - 1, ) - expected.samples = [ - Sample( - name=f"{metrics_prefix}_received_messages_in_process", - labels={"app_name": app_name, "broker": broker, "handler": queue}, - value=float(messages_amount - 1), - timestamp=None, - exemplar=None, - ), - ] manager.add_received_message_in_process( amount=messages_amount, broker=broker, handler=queue @@ -259,13 +201,13 @@ def test_remove_received_message_in_process( @pytest.mark.parametrize( "status", - [ + ( pytest.param(ProcessingStatus.acked, id="acked status"), pytest.param(ProcessingStatus.nacked, id="nacked status"), pytest.param(ProcessingStatus.rejected, id="rejected status"), pytest.param(ProcessingStatus.skipped, id="skipped status"), pytest.param(ProcessingStatus.error, id="error status"), - ], + ), ) def test_add_received_processed_message( self, @@ -281,38 +223,14 @@ def test_add_received_processed_message( metrics_prefix=metrics_prefix, ) - expected = Metric( - name=f"{metrics_prefix}_received_processed_messages", - documentation="Count of received processed messages by broker, handler and status", - unit="", - typ="counter", - ) - expected.samples = [ - Sample( - name=f"{metrics_prefix}_received_processed_messages_total", - labels={ - "app_name": app_name, - "broker": broker, - "handler": queue, - "status": status.value, - }, - value=float(messages_amount), - timestamp=None, - exemplar=None, - ), - Sample( - name=f"{metrics_prefix}_received_processed_messages_created", - labels={ - "app_name": app_name, - "broker": broker, - "handler": queue, - "status": status.value, - }, - value=IsPositiveFloat, - timestamp=None, - exemplar=None, - ), - ] + expected = get_received_processed_messages_metric( + metrics_prefix=metrics_prefix, + app_name=app_name, + broker=broker, + queue=queue, + messages_amount=messages_amount, + status=status, + ) manager.add_received_processed_message( amount=messages_amount, @@ -339,50 +257,13 @@ def test_observe_received_processed_message_duration( duration = 0.001 - expected = Metric( - name=f"{metrics_prefix}_received_processed_messages_duration_seconds", - documentation="Histogram of received processed messages duration in seconds by broker and handler", - unit="", - typ="histogram", - ) - expected.samples = [ - *[ - Sample( - name=f"{metrics_prefix}_received_processed_messages_duration_seconds_bucket", - labels={ - "app_name": app_name, - "broker": broker, - "handler": queue, - "le": IsStr, - }, - value=1.0, - timestamp=None, - exemplar=None, - ) - for _ in Histogram.DEFAULT_BUCKETS - ], - Sample( - name=f"{metrics_prefix}_received_processed_messages_duration_seconds_count", - labels={"app_name": app_name, "broker": broker, "handler": queue}, - value=1.0, - timestamp=None, - exemplar=None, - ), - Sample( - name=f"{metrics_prefix}_received_processed_messages_duration_seconds_sum", - labels={"app_name": app_name, "broker": broker, "handler": queue}, - value=duration, - timestamp=None, - exemplar=None, - ), - Sample( - name=f"{metrics_prefix}_received_processed_messages_duration_seconds_created", - labels={"app_name": app_name, "broker": broker, "handler": queue}, - value=ANY, - timestamp=None, - exemplar=None, - ), - ] + expected = get_received_processed_messages_duration_seconds_metric( + metrics_prefix=metrics_prefix, + app_name=app_name, + broker=broker, + queue=queue, + duration=duration, + ) manager.observe_received_processed_message_duration( duration=duration, @@ -409,38 +290,14 @@ def test_add_received_processed_message_exception( metrics_prefix=metrics_prefix, ) - expected = Metric( - name=f"{metrics_prefix}_received_processed_messages_exceptions", - documentation="Count of received processed messages exceptions by broker, handler and exception_type", - unit="", - typ="counter", - ) - expected.samples = [ - Sample( - name=f"{metrics_prefix}_received_processed_messages_exceptions_total", - labels={ - "app_name": app_name, - "broker": broker, - "handler": queue, - "exception_type": exception_type, - }, - value=1.0, - timestamp=None, - exemplar=None, - ), - Sample( - name=f"{metrics_prefix}_received_processed_messages_exceptions_created", - labels={ - "app_name": app_name, - "broker": broker, - "handler": queue, - "exception_type": exception_type, - }, - value=IsPositiveFloat, - timestamp=None, - exemplar=None, - ), - ] + expected = get_received_processed_messages_exceptions_metric( + metrics_prefix=metrics_prefix, + app_name=app_name, + broker=broker, + queue=queue, + exception_type=exception_type, + exceptions_amount=1, + ) manager.add_received_processed_message_exception( exception_type=exception_type, @@ -456,10 +313,10 @@ def test_add_received_processed_message_exception( @pytest.mark.parametrize( "status", - [ + ( pytest.param(PublishingStatus.success, id="success status"), pytest.param(PublishingStatus.error, id="error status"), - ], + ), ) def test_add_published_message( self, @@ -475,38 +332,14 @@ def test_add_published_message( metrics_prefix=metrics_prefix, ) - expected = Metric( - name=f"{metrics_prefix}_published_messages", - documentation="Count of published messages by destination and status", - unit="", - typ="counter", - ) - expected.samples = [ - Sample( - name=f"{metrics_prefix}_published_messages_total", - labels={ - "app_name": app_name, - "broker": broker, - "destination": queue, - "status": status.value, - }, - value=1.0, - timestamp=None, - exemplar=None, - ), - Sample( - name=f"{metrics_prefix}_published_messages_created", - labels={ - "app_name": app_name, - "broker": broker, - "destination": queue, - "status": status.value, - }, - value=IsPositiveFloat, - timestamp=None, - exemplar=None, - ), - ] + expected = get_published_messages_metric( + metrics_prefix=metrics_prefix, + app_name=app_name, + broker=broker, + queue=queue, + status=status, + messages_amount=1, + ) manager.add_published_message( status=status, @@ -532,50 +365,13 @@ def test_observe_published_message_duration( duration = 0.001 - expected = Metric( - name=f"{metrics_prefix}_published_messages_duration_seconds", - documentation="Histogram of published messages duration in seconds by broker and destination", - unit="", - typ="histogram", - ) - expected.samples = [ - *[ - Sample( - name=f"{metrics_prefix}_published_messages_duration_seconds_bucket", - labels={ - "app_name": app_name, - "broker": broker, - "destination": queue, - "le": IsStr, - }, - value=1.0, - timestamp=None, - exemplar=None, - ) - for _ in Histogram.DEFAULT_BUCKETS - ], - Sample( - name=f"{metrics_prefix}_published_messages_duration_seconds_count", - labels={"app_name": app_name, "broker": broker, "destination": queue}, - value=1.0, - timestamp=None, - exemplar=None, - ), - Sample( - name=f"{metrics_prefix}_published_messages_duration_seconds_sum", - labels={"app_name": app_name, "broker": broker, "destination": queue}, - value=duration, - timestamp=None, - exemplar=None, - ), - Sample( - name=f"{metrics_prefix}_published_messages_duration_seconds_created", - labels={"app_name": app_name, "broker": broker, "destination": queue}, - value=IsPositiveFloat, - timestamp=None, - exemplar=None, - ), - ] + expected = get_published_messages_duration_seconds_metric( + metrics_prefix=metrics_prefix, + app_name=app_name, + broker=broker, + queue=queue, + duration=duration, + ) manager.observe_published_message_duration( duration=duration, @@ -600,38 +396,13 @@ def test_add_published_message_exception( metrics_prefix=metrics_prefix, ) - expected = Metric( - name=f"{metrics_prefix}_published_messages_exceptions", - documentation="Count of published messages exceptions by broker, destination and exception_type", - unit="", - typ="counter", - ) - expected.samples = [ - Sample( - name=f"{metrics_prefix}_published_messages_exceptions_total", - labels={ - "app_name": app_name, - "broker": broker, - "destination": queue, - "exception_type": exception_type, - }, - value=1.0, - timestamp=None, - exemplar=None, - ), - Sample( - name=f"{metrics_prefix}_published_messages_exceptions_created", - labels={ - "app_name": app_name, - "broker": broker, - "destination": queue, - "exception_type": exception_type, - }, - value=IsPositiveFloat, - timestamp=None, - exemplar=None, - ), - ] + expected = get_published_messages_exceptions_metric( + metrics_prefix=metrics_prefix, + app_name=app_name, + broker=broker, + queue=queue, + exception_type=exception_type, + ) manager.add_published_message_exception( exception_type=exception_type, diff --git a/tests/prometheus/utils.py b/tests/prometheus/utils.py new file mode 100644 index 0000000000..8665f42f7c --- /dev/null +++ b/tests/prometheus/utils.py @@ -0,0 +1,426 @@ +from collections.abc import Sequence +from typing import cast + +from dirty_equals import IsFloat, IsPositiveFloat, IsStr +from prometheus_client import Histogram, Metric +from prometheus_client.samples import Sample + +from faststream.prometheus.types import ProcessingStatus, PublishingStatus + + +def get_received_messages_metric( + *, + metrics_prefix: str, + app_name: str, + broker: str, + queue: str, + messages_amount: int, +) -> Metric: + metric = Metric( + name=f"{metrics_prefix}_received_messages", + documentation="Count of received messages by broker and handler", + unit="", + typ="counter", + ) + metric.samples = [ + Sample( + name=f"{metrics_prefix}_received_messages_total", + labels={"app_name": app_name, "broker": broker, "handler": queue}, + value=float(messages_amount), + timestamp=None, + exemplar=None, + ), + Sample( + name=f"{metrics_prefix}_received_messages_created", + labels={"app_name": app_name, "broker": broker, "handler": queue}, + value=cast("float", IsPositiveFloat), + timestamp=None, + exemplar=None, + ), + ] + + return metric + + +def get_received_messages_size_bytes_metric( + *, + metrics_prefix: str, + app_name: str, + broker: str, + queue: str, + buckets: Sequence[float], + size: int, + messages_amount: int, +) -> Metric: + metric = Metric( + name=f"{metrics_prefix}_received_messages_size_bytes", + documentation="Histogram of received messages size in bytes by broker and handler", + unit="", + typ="histogram", + ) + metric.samples = [ + *[ + Sample( + name=f"{metrics_prefix}_received_messages_size_bytes_bucket", + labels={ + "app_name": app_name, + "broker": broker, + "handler": queue, + "le": cast("str", IsStr), + }, + value=float(messages_amount), + timestamp=None, + exemplar=None, + ) + for _ in buckets + ], + Sample( + name=f"{metrics_prefix}_received_messages_size_bytes_count", + labels={"app_name": app_name, "broker": broker, "handler": queue}, + value=float(messages_amount), + timestamp=None, + exemplar=None, + ), + Sample( + name=f"{metrics_prefix}_received_messages_size_bytes_sum", + labels={"app_name": app_name, "broker": broker, "handler": queue}, + value=size, + timestamp=None, + exemplar=None, + ), + Sample( + name=f"{metrics_prefix}_received_messages_size_bytes_created", + labels={"app_name": app_name, "broker": broker, "handler": queue}, + value=cast("float", IsPositiveFloat), + timestamp=None, + exemplar=None, + ), + ] + + return metric + + +def get_received_messages_in_process_metric( + *, + metrics_prefix: str, + app_name: str, + broker: str, + queue: str, + messages_amount: int, +) -> Metric: + metric = Metric( + name=f"{metrics_prefix}_received_messages_in_process", + documentation="Gauge of received messages in process by broker and handler", + unit="", + typ="gauge", + ) + metric.samples = [ + Sample( + name=f"{metrics_prefix}_received_messages_in_process", + labels={"app_name": app_name, "broker": broker, "handler": queue}, + value=float(messages_amount), + timestamp=None, + exemplar=None, + ), + ] + + return metric + + +def get_received_processed_messages_metric( + *, + metrics_prefix: str, + app_name: str, + broker: str, + queue: str, + messages_amount: int, + status: ProcessingStatus, +) -> Metric: + metric = Metric( + name=f"{metrics_prefix}_received_processed_messages", + documentation="Count of received processed messages by broker, handler and status", + unit="", + typ="counter", + ) + metric.samples = [ + Sample( + name=f"{metrics_prefix}_received_processed_messages_total", + labels={ + "app_name": app_name, + "broker": broker, + "handler": queue, + "status": status.value, + }, + value=float(messages_amount), + timestamp=None, + exemplar=None, + ), + Sample( + name=f"{metrics_prefix}_received_processed_messages_created", + labels={ + "app_name": app_name, + "broker": broker, + "handler": queue, + "status": status.value, + }, + value=cast("float", IsPositiveFloat), + timestamp=None, + exemplar=None, + ), + ] + + return metric + + +def get_received_processed_messages_duration_seconds_metric( + *, + metrics_prefix: str, + app_name: str, + broker: str, + queue: str, + duration: float, +) -> Metric: + metric = Metric( + name=f"{metrics_prefix}_received_processed_messages_duration_seconds", + documentation="Histogram of received processed messages duration in seconds by broker and handler", + unit="", + typ="histogram", + ) + metric.samples = [ + *[ + Sample( + name=f"{metrics_prefix}_received_processed_messages_duration_seconds_bucket", + labels={ + "app_name": app_name, + "broker": broker, + "handler": queue, + "le": cast("str", IsStr), + }, + value=cast("float", IsFloat), + timestamp=None, + exemplar=None, + ) + for _ in Histogram.DEFAULT_BUCKETS + ], + Sample( + name=f"{metrics_prefix}_received_processed_messages_duration_seconds_count", + labels={"app_name": app_name, "broker": broker, "handler": queue}, + value=cast("float", IsPositiveFloat), + timestamp=None, + exemplar=None, + ), + Sample( + name=f"{metrics_prefix}_received_processed_messages_duration_seconds_sum", + labels={"app_name": app_name, "broker": broker, "handler": queue}, + value=duration, + timestamp=None, + exemplar=None, + ), + Sample( + name=f"{metrics_prefix}_received_processed_messages_duration_seconds_created", + labels={"app_name": app_name, "broker": broker, "handler": queue}, + value=cast("float", IsPositiveFloat), + timestamp=None, + exemplar=None, + ), + ] + + return metric + + +def get_received_processed_messages_exceptions_metric( + *, + metrics_prefix: str, + app_name: str, + broker: str, + queue: str, + exception_type: str | None, + exceptions_amount: int, +) -> Metric: + metric = Metric( + name=f"{metrics_prefix}_received_processed_messages_exceptions", + documentation="Count of received processed messages exceptions by broker, handler and exception_type", + unit="", + typ="counter", + ) + metric.samples = ( + [ + Sample( + name=f"{metrics_prefix}_received_processed_messages_exceptions_total", + labels={ + "app_name": app_name, + "broker": broker, + "handler": queue, + "exception_type": exception_type, + }, + value=float(exceptions_amount), + timestamp=None, + exemplar=None, + ), + Sample( + name=f"{metrics_prefix}_received_processed_messages_exceptions_created", + labels={ + "app_name": app_name, + "broker": broker, + "handler": queue, + "exception_type": exception_type, + }, + value=cast("float", IsPositiveFloat), + timestamp=None, + exemplar=None, + ), + ] + if exception_type is not None + else [] + ) + + return metric + + +def get_published_messages_metric( + *, + metrics_prefix: str, + app_name: str, + broker: str, + queue: str, + messages_amount: int, + status: PublishingStatus, +) -> Metric: + metric = Metric( + name=f"{metrics_prefix}_published_messages", + documentation="Count of published messages by destination and status", + unit="", + typ="counter", + ) + metric.samples = [ + Sample( + name=f"{metrics_prefix}_published_messages_total", + labels={ + "app_name": app_name, + "broker": broker, + "destination": queue, + "status": status.value, + }, + value=messages_amount, + timestamp=None, + exemplar=None, + ), + Sample( + name=f"{metrics_prefix}_published_messages_created", + labels={ + "app_name": app_name, + "broker": broker, + "destination": queue, + "status": status.value, + }, + value=cast("float", IsPositiveFloat), + timestamp=None, + exemplar=None, + ), + ] + + return metric + + +def get_published_messages_duration_seconds_metric( + *, + metrics_prefix: str, + app_name: str, + broker: str, + queue: str, + duration: float, +) -> Metric: + metric = Metric( + name=f"{metrics_prefix}_published_messages_duration_seconds", + documentation="Histogram of published messages duration in seconds by broker and destination", + unit="", + typ="histogram", + ) + metric.samples = [ + *[ + Sample( + name=f"{metrics_prefix}_published_messages_duration_seconds_bucket", + labels={ + "app_name": app_name, + "broker": broker, + "destination": queue, + "le": cast("str", IsStr), + }, + value=cast("float", IsFloat), + timestamp=None, + exemplar=None, + ) + for _ in Histogram.DEFAULT_BUCKETS + ], + Sample( + name=f"{metrics_prefix}_published_messages_duration_seconds_count", + labels={"app_name": app_name, "broker": broker, "destination": queue}, + value=cast("float", IsPositiveFloat), + timestamp=None, + exemplar=None, + ), + Sample( + name=f"{metrics_prefix}_published_messages_duration_seconds_sum", + labels={"app_name": app_name, "broker": broker, "destination": queue}, + value=duration, + timestamp=None, + exemplar=None, + ), + Sample( + name=f"{metrics_prefix}_published_messages_duration_seconds_created", + labels={"app_name": app_name, "broker": broker, "destination": queue}, + value=cast("float", IsPositiveFloat), + timestamp=None, + exemplar=None, + ), + ] + + return metric + + +def get_published_messages_exceptions_metric( + *, + metrics_prefix: str, + app_name: str, + broker: str, + queue: str, + exception_type: str | None, +) -> Metric: + metric = Metric( + name=f"{metrics_prefix}_published_messages_exceptions", + documentation="Count of published messages exceptions by broker, destination and exception_type", + unit="", + typ="counter", + ) + metric.samples = ( + [ + Sample( + name=f"{metrics_prefix}_published_messages_exceptions_total", + labels={ + "app_name": app_name, + "broker": broker, + "destination": queue, + "exception_type": exception_type, + }, + value=1.0, + timestamp=None, + exemplar=None, + ), + Sample( + name=f"{metrics_prefix}_published_messages_exceptions_created", + labels={ + "app_name": app_name, + "broker": broker, + "destination": queue, + "exception_type": exception_type, + }, + value=cast("float", IsPositiveFloat), + timestamp=None, + exemplar=None, + ), + ] + if exception_type is not None + else [] + ) + + return metric diff --git a/tests/tools.py b/tests/tools.py index 3ff9f57186..d49679f20c 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -1,35 +1,37 @@ import inspect +from collections.abc import Callable from functools import wraps -from typing import Any, Iterable +from typing import Protocol, TypeVar from unittest.mock import MagicMock +from typing_extensions import ParamSpec -def spy_decorator(method): +P = ParamSpec("P") +T = TypeVar("T") + + +class SmartMock(Protocol[P, T]): + mock: MagicMock + + def __call__(self, *args: P.args, **kwds: P.kwargs) -> T: ... + + +def spy_decorator(method: Callable[P, T]) -> SmartMock[P, T]: mock = MagicMock() if inspect.iscoroutinefunction(method): @wraps(method) - async def wrapper(*args, **kwargs): + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: mock(*args, **kwargs) return await method(*args, **kwargs) + else: @wraps(method) - def wrapper(*args, **kwargs): + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: mock(*args, **kwargs) return method(*args, **kwargs) wrapper.mock = mock return wrapper - - -class AsyncIterator: - def __init__(self, iterable: Iterable[Any]) -> None: - self.iter = iter(iterable) - - def __aiter__(self): - return self - - async def __anext__(self): - return next(self.iter) diff --git a/tests/utils/context/test_alias.py b/tests/utils/context/test_alias.py index 55590cea2f..a84e475a03 100644 --- a/tests/utils/context/test_alias.py +++ b/tests/utils/context/test_alias.py @@ -1,41 +1,41 @@ -from typing import Any +from typing import Annotated, Any import pytest -from typing_extensions import Annotated -from faststream.utils import Context, ContextRepo, apply_types +from faststream import Context, ContextRepo +from faststream._internal.utils import apply_types -@pytest.mark.asyncio -async def test_base_context_alias(context: ContextRepo): +@pytest.mark.asyncio() +async def test_base_context_alias(context: ContextRepo) -> None: key = 1000 context.set_global("key", key) - @apply_types + @apply_types(context__=context) async def func(k=Context("key")): return k is key assert await func() -@pytest.mark.asyncio -async def test_context_cast(context: ContextRepo): +@pytest.mark.asyncio() +async def test_context_cast(context: ContextRepo) -> None: key = 1000 context.set_global("key", key) - @apply_types + @apply_types(context__=context) async def func(k: float = Context("key", cast=True)): return isinstance(k, float) assert await func() -@pytest.mark.asyncio -async def test_nested_context_alias(context: ContextRepo): +@pytest.mark.asyncio() +async def test_nested_context_alias(context: ContextRepo) -> None: model = SomeModel(field=SomeModel(field=1000)) context.set_global("model", model) - @apply_types + @apply_types(context__=context) async def func( m=Context("model.field.field"), m2=Context("model.not_existed", default=None), @@ -54,12 +54,12 @@ async def func( assert await func(model=model) -@pytest.mark.asyncio -async def test_annotated_alias(context: ContextRepo): +@pytest.mark.asyncio() +async def test_annotated_alias(context: ContextRepo) -> None: model = SomeModel(field=SomeModel(field=1000)) context.set_global("model", model) - @apply_types + @apply_types(context__=context) async def func(m: Annotated[int, Context("model.field.field")]): return m is model.field.field @@ -70,5 +70,5 @@ class SomeModel: field: Any = "" another_field: Any = None - def __init__(self, field): + def __init__(self, field) -> None: self.field = field diff --git a/tests/utils/context/test_depends.py b/tests/utils/context/test_depends.py index b2c92434a9..942a7b5afe 100644 --- a/tests/utils/context/test_depends.py +++ b/tests/utils/context/test_depends.py @@ -1,7 +1,9 @@ +from typing import Annotated + import pytest -from typing_extensions import Annotated -from faststream.utils import Depends, apply_types +from faststream import Depends +from faststream._internal.utils import apply_types def sync_dep(key): @@ -12,8 +14,8 @@ async def async_dep(key): return key -@pytest.mark.asyncio -async def test_sync_depends(): +@pytest.mark.asyncio() +async def test_sync_depends() -> None: key = 1000 @apply_types @@ -23,17 +25,17 @@ def func(k=Depends(sync_dep)): assert func(key=key) -@pytest.mark.asyncio -async def test_sync_with_async_depends(): +@pytest.mark.asyncio() +async def test_sync_with_async_depends() -> None: with pytest.raises(AssertionError): @apply_types - def func(k=Depends(async_dep)): # pragma: no cover + def func(k=Depends(async_dep)) -> None: # pragma: no cover pass -@pytest.mark.asyncio -async def test_async_depends(): +@pytest.mark.asyncio() +async def test_async_depends() -> None: key = 1000 @apply_types @@ -43,8 +45,8 @@ async def func(k=Depends(async_dep)): assert await func(key=key) -@pytest.mark.asyncio -async def test_async_with_sync_depends(): +@pytest.mark.asyncio() +async def test_async_with_sync_depends() -> None: key = 1000 @apply_types @@ -54,8 +56,8 @@ async def func(k=Depends(sync_dep)): assert await func(key=key) -@pytest.mark.asyncio -async def test_annotated_depends(): +@pytest.mark.asyncio() +async def test_annotated_depends() -> None: D = Annotated[int, Depends(sync_dep)] # noqa: N806 key = 1000 diff --git a/tests/utils/context/test_headers.py b/tests/utils/context/test_headers.py index fa6c716db9..bb694ddf36 100644 --- a/tests/utils/context/test_headers.py +++ b/tests/utils/context/test_headers.py @@ -4,9 +4,9 @@ from tests.marks import require_nats -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_nats_headers(): +async def test_nats_headers() -> None: from faststream.nats import NatsBroker, TestNatsBroker broker = NatsBroker() @@ -15,22 +15,23 @@ async def test_nats_headers(): async def h( name: str = Header(), id_: int = Header("id"), - ): + ) -> int: assert name == "john" assert id_ == 1 return 1 async with TestNatsBroker(broker) as br: assert ( - await br.publish( - "", - "in", - headers={ - "name": "john", - "id": "1", - }, - rpc=True, - rpc_timeout=1.0, - ) + await ( + await br.request( + "", + "in", + headers={ + "name": "john", + "id": "1", + }, + timeout=1.0, + ) + ).decode() == 1 ) diff --git a/tests/utils/context/test_main.py b/tests/utils/context/test_main.py index 39e6434cec..c34317a879 100644 --- a/tests/utils/context/test_main.py +++ b/tests/utils/context/test_main.py @@ -1,10 +1,11 @@ import pytest -from pydantic import ValidationError +from fast_depends.exceptions import ValidationError -from faststream.utils import Context, ContextRepo, apply_types +from faststream import Context, ContextRepo +from faststream._internal.utils import apply_types -def test_context_getattr(context: ContextRepo): +def test_context_getattr(context: ContextRepo) -> None: a = 1000 context.set_global("key", a) @@ -12,117 +13,117 @@ def test_context_getattr(context: ContextRepo): assert context.key2 is None -@pytest.mark.asyncio -async def test_context_apply(context: ContextRepo): +@pytest.mark.asyncio() +async def test_context_apply(context: ContextRepo) -> None: a = 1000 context.set_global("key", a) - @apply_types + @apply_types(context__=context) async def use(key=Context()): return key is a assert await use() -@pytest.mark.asyncio -async def test_context_ignore(context: ContextRepo): +@pytest.mark.asyncio() +async def test_context_ignore(context: ContextRepo) -> None: a = 3 context.set_global("key", a) - @apply_types - async def use(): + @apply_types(context__=context) + async def use() -> None: return None assert await use() is None -@pytest.mark.asyncio -async def test_context_apply_multi(context: ContextRepo): +@pytest.mark.asyncio() +async def test_context_apply_multi(context: ContextRepo) -> None: a = 1001 context.set_global("key_a", a) b = 1000 context.set_global("key_b", b) - @apply_types + @apply_types(context__=context) async def use1(key_a=Context()): return key_a is a assert await use1() - @apply_types + @apply_types(context__=context) async def use2(key_b=Context()): return key_b is b assert await use2() - @apply_types + @apply_types(context__=context) async def use3(key_a=Context(), key_b=Context()): return key_a is a and key_b is b assert await use3() -@pytest.mark.asyncio -async def test_context_overrides(context: ContextRepo): +@pytest.mark.asyncio() +async def test_context_overrides(context: ContextRepo) -> None: a = 1001 context.set_global("test", a) b = 1000 context.set_global("test", b) - @apply_types + @apply_types(context__=context) async def use(test=Context()): return test is b assert await use() -@pytest.mark.asyncio -async def test_context_nested_apply(context: ContextRepo): +@pytest.mark.asyncio() +async def test_context_nested_apply(context: ContextRepo) -> None: a = 1000 context.set_global("key", a) - @apply_types + @apply_types(context__=context) def use_nested(key=Context()): return key - @apply_types + @apply_types(context__=context) async def use(key=Context()): return key is use_nested() is a assert await use() -@pytest.mark.asyncio -async def test_reset_global(context: ContextRepo): +@pytest.mark.asyncio() +async def test_reset_global(context: ContextRepo) -> None: a = 1000 context.set_global("key", a) context.reset_global("key") - @apply_types - async def use(key=Context()): ... + @apply_types(context__=context) + async def use(key=Context()) -> None: ... with pytest.raises(ValidationError): await use() -@pytest.mark.asyncio -async def test_clear_context(context: ContextRepo): +@pytest.mark.asyncio() +async def test_clear_context(context: ContextRepo) -> None: a = 1000 context.set_global("key", a) context.clear() - @apply_types + @apply_types(context__=context) async def use(key=Context(default=None)): return key is None assert await use() -def test_scope(context: ContextRepo): - @apply_types - def use(key=Context(), key2=Context()): +def test_scope(context: ContextRepo) -> None: + @apply_types(context__=context) + def use(key=Context(), key2=Context()) -> None: assert key == 1 assert key2 == 1 @@ -133,28 +134,33 @@ def use(key=Context(), key2=Context()): assert context.get("key2") is None -def test_default(context: ContextRepo): - @apply_types +def test_default(context: ContextRepo) -> None: + @apply_types(context__=context) def use( key=Context(), key2=Context(), key3=Context(default=1), key4=Context("key.key4", default=1), key5=Context("key5.key6"), - ): + ) -> None: assert key == 0 assert key2 is True assert key3 == 1 assert key4 == 1 assert key5 is False - with context.scope("key", 0), context.scope("key2", True), context.scope( - "key5", {"key6": False} + with ( + context.scope("key", 0), + context.scope("key2", True), + context.scope( + "key5", + {"key6": False}, + ), ): use() -def test_local_default(context: ContextRepo): +def test_local_default(context: ContextRepo) -> None: key = "some-key" tag = context.set_local(key, "useless") @@ -163,8 +169,8 @@ def test_local_default(context: ContextRepo): assert context.get_local(key, 1) == 1 -def test_initial(): - @apply_types +def test_initial(context: ContextRepo) -> None: + @apply_types(context__=context) def use( a, key=Context(initial=list), @@ -176,10 +182,12 @@ def use( assert use(2) == [1, 2] -@pytest.mark.asyncio -async def test_context_with_custom_object_implementing_comparison(context: ContextRepo): +@pytest.mark.asyncio() +async def test_context_with_custom_object_implementing_comparison( + context: ContextRepo, +) -> None: class User: - def __init__(self, user_id: int): + def __init__(self, user_id: int) -> None: self.user_id = user_id def __eq__(self, other): @@ -193,7 +201,7 @@ def __ne__(self, other): user2 = User(user_id=2) user3 = User(user_id=3) - @apply_types + @apply_types(context__=context) async def use( key1=Context("user1"), key2=Context("user2", default=user2), @@ -205,7 +213,8 @@ async def use( and key3 == User(user_id=4) ) - with context.scope("user1", User(user_id=1)), context.scope( - "user3", User(user_id=4) + with ( + context.scope("user1", User(user_id=1)), + context.scope("user3", User(user_id=4)), ): assert await use() diff --git a/tests/utils/context/test_path.py b/tests/utils/context/test_path.py index 5cfc8caf99..719a36f1f0 100644 --- a/tests/utils/context/test_path.py +++ b/tests/utils/context/test_path.py @@ -1,5 +1,5 @@ import asyncio -from unittest.mock import Mock +from unittest.mock import MagicMock import pytest @@ -7,9 +7,9 @@ from tests.marks import require_aiokafka, require_aiopika, require_nats, require_redis -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiokafka -async def test_aiokafka_path(): +async def test_aiokafka_path() -> None: from faststream.kafka import KafkaBroker, TestKafkaBroker broker = KafkaBroker() @@ -18,26 +18,27 @@ async def test_aiokafka_path(): async def h( name: str = Path(), id_: int = Path("id"), - ): + ) -> int: assert name == "john" assert id_ == 1 return 1 async with TestKafkaBroker(broker) as br: assert ( - await br.publish( - "", - "in.john.1", - rpc=True, - rpc_timeout=1.0, - ) + await ( + await br.request( + "", + "in.john.1", + timeout=1.0, + ) + ).decode() == 1 ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_nats_path(): +async def test_nats_path() -> None: from faststream.nats import NatsBroker, TestNatsBroker broker = NatsBroker() @@ -46,31 +47,33 @@ async def test_nats_path(): async def h( name: str = Path(), id_: int = Path("id"), - ): + ) -> int: assert name == "john" assert id_ == 1 return 1 async with TestNatsBroker(broker) as br: assert ( - await br.publish( - "", - "in.john.1", - rpc=True, - rpc_timeout=1.0, - ) + await ( + await br.request( + "", + "in.john.1", + timeout=1.0, + ) + ).decode() == 1 ) -@pytest.mark.asyncio -@pytest.mark.nats +@pytest.mark.asyncio() +@pytest.mark.nats() @require_nats async def test_nats_kv_path( queue: str, - event: asyncio.Event, - mock: Mock, -): + mock: MagicMock, +) -> None: + event = asyncio.Event() + from faststream.nats import NatsBroker broker = NatsBroker() @@ -80,7 +83,7 @@ async def h( msg: int, name: str = Path(), id_: int = Path("id"), - ): + ) -> None: mock(msg == 1 and name == "john" and id_ == 1) event.set() @@ -101,9 +104,9 @@ async def h( mock.assert_called_once_with(True) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_nats -async def test_nats_batch_path(): +async def test_nats_batch_path() -> None: from faststream.nats import NatsBroker, PullSub, TestNatsBroker broker = NatsBroker() @@ -112,26 +115,27 @@ async def test_nats_batch_path(): async def h( name: str = Path(), id_: int = Path("id"), - ): + ) -> int: assert name == "john" assert id_ == 1 return 1 async with TestNatsBroker(broker) as br: assert ( - await br.publish( - "", - "in.john.1", - rpc=True, - rpc_timeout=1.0, - ) + await ( + await br.request( + "", + "in.john.1", + timeout=1.0, + ) + ).decode() == 1 ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_redis -async def test_redis_path(): +async def test_redis_path() -> None: from faststream.redis import RedisBroker, TestRedisBroker broker = RedisBroker() @@ -140,26 +144,27 @@ async def test_redis_path(): async def h( name: str = Path(), id_: int = Path("id"), - ): + ) -> int: assert name == "john" assert id_ == 1 return 1 async with TestRedisBroker(broker) as br: assert ( - await br.publish( - "", - "in.john.1", - rpc=True, - rpc_timeout=1.0, - ) + await ( + await br.request( + "", + "in.john.1", + timeout=1.0, + ) + ).decode() == 1 ) -@pytest.mark.asyncio +@pytest.mark.asyncio() @require_aiopika -async def test_rabbit_path(): +async def test_rabbit_path() -> None: from faststream.rabbit import ( ExchangeType, RabbitBroker, @@ -183,19 +188,20 @@ async def test_rabbit_path(): async def h( name: str = Path(), id_: int = Path("id"), - ): + ) -> int: assert name == "john" assert id_ == 1 return 1 async with TestRabbitBroker(broker) as br: assert ( - await br.publish( - "", - "in.john.1", - "test", - rpc=True, - rpc_timeout=1.0, - ) + await ( + await br.request( + "", + "in.john.1", + "test", + timeout=1.0, + ) + ).decode() == 1 ) diff --git a/tests/utils/test_ast.py b/tests/utils/test_ast.py index 6417425c90..d57d29e88f 100644 --- a/tests/utils/test_ast.py +++ b/tests/utils/test_ast.py @@ -1,6 +1,6 @@ import pytest -from faststream.utils.ast import is_contains_context_name +from faststream._internal.testing.ast import is_contains_context_name class Context: @@ -18,65 +18,65 @@ async def __aexit__(self, *args): class A(Context): - def __init__(self): + def __init__(self) -> None: self.contains = is_contains_context_name(self.__class__.__name__, B.__name__) class B(Context): - def __init__(self): + def __init__(self) -> None: pass -def test_base(): +def test_base() -> None: with A() as a, B(): assert a.contains -@pytest.mark.asyncio -async def test_base_async(): +@pytest.mark.asyncio() +async def test_base_async() -> None: async with A() as a, B(): assert a.contains -def test_nested(): +def test_nested() -> None: with A() as a, B(): assert a.contains -@pytest.mark.asyncio -async def test_nested_async(): +@pytest.mark.asyncio() +async def test_nested_async() -> None: async with A() as a, B(): assert a.contains -@pytest.mark.asyncio -async def test_async_A(): # noqa: N802 +@pytest.mark.asyncio() +async def test_async_A() -> None: # noqa: N802 async with A() as a: with B(): assert a.contains -@pytest.mark.asyncio -async def test_async_B(): # noqa: N802 +@pytest.mark.asyncio() +async def test_async_B() -> None: # noqa: N802 with A() as a: async with B(): assert a.contains -def test_base_invalid(): +def test_base_invalid() -> None: with B(), B(), A() as a: assert not a.contains -def test_nested_invalid(): +def test_nested_invalid() -> None: with B(), A() as a: assert not a.contains -def test_not_broken(): +def test_not_broken() -> None: with A() as a, B(): assert a.contains # test ast processes another context correctly with pytest.raises(ValueError): # noqa: PT011 - raise ValueError() + raise ValueError diff --git a/tests/utils/test_classes.py b/tests/utils/test_classes.py deleted file mode 100644 index 65d1a3bc8a..0000000000 --- a/tests/utils/test_classes.py +++ /dev/null @@ -1,11 +0,0 @@ -from faststream.utils.classes import Singleton - - -def test_singleton(): - assert Singleton() is Singleton() - - -def test_drop(): - s1 = Singleton() - s1._drop() - assert Singleton() is not s1 diff --git a/tests/utils/test_functions.py b/tests/utils/test_functions.py index 5b3cf8a57e..780aea2526 100644 --- a/tests/utils/test_functions.py +++ b/tests/utils/test_functions.py @@ -1,6 +1,6 @@ import pytest -from faststream.utils.functions import call_or_await +from faststream._internal.utils.functions import call_or_await def sync_func(a): @@ -11,11 +11,11 @@ async def async_func(a): return a -@pytest.mark.asyncio -async def test_call(): +@pytest.mark.asyncio() +async def test_call() -> None: assert (await call_or_await(sync_func, a=3)) == 3 -@pytest.mark.asyncio -async def test_await(): +@pytest.mark.asyncio() +async def test_await() -> None: assert (await call_or_await(async_func, a=3)) == 3 diff --git a/tests/utils/test_handler_lock.py b/tests/utils/test_handler_lock.py index 814b606f46..60fa4158be 100644 --- a/tests/utils/test_handler_lock.py +++ b/tests/utils/test_handler_lock.py @@ -4,11 +4,11 @@ import pytest from anyio.abc import TaskStatus -from faststream.broker.utils import MultiLock +from faststream._internal.endpoint.subscriber.utils import MultiLock -@pytest.mark.asyncio -async def test_base(): +@pytest.mark.asyncio() +async def test_base() -> None: lock = MultiLock() with lock: @@ -26,15 +26,15 @@ async def test_base(): assert lock.qsize == 0 -@pytest.mark.asyncio -async def test_wait_correct(): +@pytest.mark.asyncio() +async def test_wait_correct() -> None: lock = MultiLock() - async def func(): + async def func() -> None: with lock: await asyncio.sleep(0.01) - async def check(task_status: TaskStatus): + async def check(task_status: TaskStatus) -> None: task_status.started() assert not lock.empty @@ -50,15 +50,15 @@ async def check(task_status: TaskStatus): await tg.start(check) -@pytest.mark.asyncio -async def test_nowait_correct(): +@pytest.mark.asyncio() +async def test_nowait_correct() -> None: lock = MultiLock() - async def func(): + async def func() -> None: with lock: await asyncio.sleep(0.01) - async def check(task_status: TaskStatus): + async def check(task_status: TaskStatus) -> None: task_status.started() assert not lock.empty diff --git a/tests/utils/test_no_cast.py b/tests/utils/test_no_cast.py index 9fbb2f850d..62c7fe6d24 100644 --- a/tests/utils/test_no_cast.py +++ b/tests/utils/test_no_cast.py @@ -1,10 +1,10 @@ from faststream import apply_types -from faststream.annotations import NoCast +from faststream.params import NoCast -def test_no_cast(): +def test_no_cast() -> None: @apply_types - def handler(s: NoCast[str]): + def handler(s: NoCast[str]) -> None: assert isinstance(s, int) handler(1) diff --git a/tests/utils/type_cast/test_base.py b/tests/utils/type_cast/test_base.py index 150e0e68db..c973e2efdc 100644 --- a/tests/utils/type_cast/test_base.py +++ b/tests/utils/type_cast/test_base.py @@ -1,21 +1,19 @@ -from typing import Tuple - import pytest -from faststream.utils import apply_types +from faststream._internal.utils import apply_types @apply_types -def cast_int(t: int = 1) -> Tuple[bool, int]: +def cast_int(t: int = 1) -> tuple[bool, int]: return isinstance(t, int), t @apply_types -def cast_default(t: int = 1) -> Tuple[bool, int]: +def cast_default(t: int = 1) -> tuple[bool, int]: return isinstance(t, int), t -def test_int(): +def test_int() -> None: assert cast_int("1") == (True, 1) assert cast_int(t=1.0) == (True, 1) @@ -30,7 +28,7 @@ def test_int(): assert cast_int([]) -def test_cast_default(): +def test_cast_default() -> None: assert cast_default("1") == (True, 1) assert cast_default(t=1.0) == (True, 1) diff --git a/tests/utils/type_cast/test_model.py b/tests/utils/type_cast/test_model.py index 2cb57dca04..4c0465846e 100644 --- a/tests/utils/type_cast/test_model.py +++ b/tests/utils/type_cast/test_model.py @@ -1,9 +1,7 @@ -from typing import Tuple - import pytest from pydantic import BaseModel -from faststream.utils import apply_types +from faststream._internal.utils import apply_types class Base(BaseModel): @@ -11,11 +9,11 @@ class Base(BaseModel): @apply_types -def cast_model(t: Base) -> Tuple[bool, Base]: +def cast_model(t: Base) -> tuple[bool, Base]: return isinstance(t, Base), t -def test_model(): +def test_model() -> None: is_casted, m = cast_model({"field": 1}) assert is_casted, m.field == (True, 1)