Skip to content

Commit d9fd8af

Browse files
committed
Added nats integration + docker usage update.
1 parent 7120a15 commit d9fd8af

28 files changed

Lines changed: 370 additions & 48 deletions

fastapi_template/cli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,20 @@ def checker(ctx: BuilderContext) -> bool:
555555
)
556556
),
557557
),
558+
MenuEntry(
559+
code="enable_nats",
560+
cli_name="nats",
561+
user_view="Add NATS support",
562+
description=(
563+
"{what} is a message broker.\nThis message queue is {why} and very fast.".format(
564+
what=colored("NATS", color="green"),
565+
why=colored(
566+
"super flexible",
567+
color="cyan",
568+
),
569+
)
570+
),
571+
),
558572
MenuEntry(
559573
code="gunicorn",
560574
cli_name="gunicorn",

fastapi_template/template/cookiecutter.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
"enable_kafka": {
3030
"type": "bool"
3131
},
32+
"enable_nats": {
33+
"type": "bool"
34+
},
3235
"enable_loguru": {
3336
"type": "bool"
3437
},

fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"{{cookiecutter.project_name}}/web/api/dummy",
1313
"{{cookiecutter.project_name}}/web/api/echo",
1414
"{{cookiecutter.project_name}}/web/api/redis",
15-
"{{cookiecutter.project_name}}/web/api/kafka"
15+
"{{cookiecutter.project_name}}/web/api/kafka",
16+
"{{cookiecutter.project_name}}/web/api/nats"
1617
]
1718
},
1819
"Redis": {
@@ -42,6 +43,15 @@
4243
"tests/test_kafka.py"
4344
]
4445
},
46+
"Nats support": {
47+
"enabled": "{{cookiecutter.enable_nats}}",
48+
"resources": [
49+
"{{cookiecutter.project_name}}/web/api/nats",
50+
"{{cookiecutter.project_name}}/web/gql/nats",
51+
"{{cookiecutter.project_name}}/services/nats",
52+
"tests/test_nats.py"
53+
]
54+
},
4555
"Database support": {
4656
"enabled": "{{cookiecutter.db_info.name != 'none'}}",
4757
"resources": [

fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.dev.yml

Lines changed: 0 additions & 26 deletions
This file was deleted.

fastapi_template/template/{{cookiecutter.project_name}}/docker-compose.yml

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ services:
22
api: &main_app
33
build:
44
context: .
5+
target: dev
56
dockerfile: ./Dockerfile
67
image: {{cookiecutter.project_name}}:{{"${" }}{{cookiecutter.project_name | upper }}_VERSION:-latest{{"}"}}
78
restart: always
9+
ports:
10+
# Exposes application port.
11+
- "8000:8000"
812
env_file:
913
- path: .env
1014
required: false
@@ -23,7 +27,9 @@ services:
2327
{%- if ((cookiecutter.db_info.name != "none" and cookiecutter.db_info.name != "sqlite") or
2428
(cookiecutter.enable_redis == "True") or
2529
(cookiecutter.enable_rmq == "True") or
26-
(cookiecutter.enable_kafka == "True")) %}
30+
(cookiecutter.enable_kafka == "True") or
31+
(cookiecutter.enable_nats == "True")
32+
) %}
2733
depends_on:
2834
{%- if cookiecutter.db_info.name != "none" %}
2935
{%- if cookiecutter.db_info.name != "sqlite" %}
@@ -43,13 +49,18 @@ services:
4349
kafka:
4450
condition: service_healthy
4551
{%- endif %}
52+
{%- if cookiecutter.enable_nats == "True" %}
53+
nats:
54+
condition: service_healthy
55+
{%- endif %}
4656
{%- if cookiecutter.enable_migrations == 'True' and cookiecutter.orm != 'psycopg' %}
4757
migrator:
4858
condition: service_completed_successfully
4959
{%- endif %}
5060
{%- endif %}
5161
environment:
5262
{{cookiecutter.project_name | upper }}_HOST: 0.0.0.0
63+
{{cookiecutter.project_name | upper}}_RELOAD: "True"
5364
{%- if cookiecutter.db_info.name != "none" %}
5465
{%- if cookiecutter.db_info.name == "sqlite" %}
5566
{{cookiecutter.project_name | upper }}_DB_FILE: /db_data/db.sqlite3
@@ -72,12 +83,16 @@ services:
7283
{{cookiecutter.project_name | upper }}_REDIS_HOST: {{cookiecutter.project_name}}-redis
7384
{%- endif %}
7485
{%- if cookiecutter.enable_kafka == "True" %}
75-
TESTKAFKA_KAFKA_BOOTSTRAP_SERVERS: '["{{cookiecutter.project_name}}-kafka:9092"]'
86+
{{cookiecutter.project_name | upper }}_KAFKA_BOOTSTRAP_SERVERS: '["{{cookiecutter.project_name}}-kafka:9092"]'
87+
{%- endif %}
88+
{%- if cookiecutter.enable_nats == "True" %}
89+
{{cookiecutter.project_name | upper }}_NATS_HOSTS: '["nats://{{cookiecutter.project_name}}-nats:4222"]'
7690
{%- endif %}
77-
{%- if cookiecutter.db_info.name == "sqlite" %}
7891
volumes:
92+
- .:/app/src/
93+
{%- if cookiecutter.db_info.name == "sqlite" %}
7994
- {{cookiecutter.project_name}}-db-data:/db_data/
80-
{%- endif %}
95+
{%- endif %}
8196

8297
{%- if cookiecutter.enable_taskiq == "True" %}
8398

@@ -88,6 +103,7 @@ services:
88103
- taskiq
89104
- worker
90105
- {{cookiecutter.project_name}}.tkq:broker
106+
- --reload
91107
{%- endif %}
92108

93109
{%- if cookiecutter.db_info.name == "postgresql" %}
@@ -234,6 +250,25 @@ services:
234250

235251
{%- endif %}
236252

253+
{%- if cookiecutter.enable_nats == "True" %}
254+
nats:
255+
image: nats:2.12-alpine
256+
hostname: "{{cookiecutter.project_name}}-nats"
257+
command: -m 8222 -js
258+
healthcheck:
259+
test:
260+
- CMD
261+
- sh
262+
- -c
263+
- "wget http://localhost:8222/healthz -q -O - | xargs | grep ok || exit 1"
264+
interval: 5s
265+
timeout: 3s
266+
retries: 20
267+
start_period: 3s
268+
ports:
269+
- 4222:4222
270+
{%- endif %}
271+
237272
{% if cookiecutter.db_info.name != 'none' %}
238273

239274
volumes:

fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ dependencies = [
132132
{%- if cookiecutter.enable_kafka == "True" %}
133133
"aiokafka >=0.12.0,<1",
134134
{%- endif %}
135+
{%- if cookiecutter.enable_nats == "True" %}
136+
"natsrpy>=0.1,<1",
137+
{%- endif %}
135138
{%- if cookiecutter.enable_taskiq == "True" %}
136139
"taskiq >=0.12.0,<1",
137140
"taskiq-fastapi >=0.3.6,<1",

fastapi_template/template/{{cookiecutter.project_name}}/tests/conftest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232

3333
{%- endif %}
3434

35+
{%- if cookiecutter.enable_nats == "True" %}
36+
from natsrpy import Nats
37+
from {{cookiecutter.project_name}}.services.nats.dependencies import get_nats
38+
from {{cookiecutter.project_name}}.services.nats.lifespan import (init_nats,
39+
shutdown_nats)
40+
{%- endif %}
41+
3542
from {{cookiecutter.project_name}}.settings import settings
3643
from {{cookiecutter.project_name}}.web.application import get_app
3744

@@ -457,6 +464,19 @@ async def test_kafka_producer() -> AsyncGenerator[AIOKafkaProducer, None]:
457464

458465
{%- endif %}
459466

467+
468+
{%- if cookiecutter.enable_nats == "True" %}
469+
470+
@pytest.fixture
471+
async def test_nats() -> AsyncGenerator[Nats, None]:
472+
"""Creat test nats client."""
473+
app_mock = Mock()
474+
await init_nats(app_mock)
475+
yield app_mock.state.nats
476+
await shutdown_nats(app_mock)
477+
478+
{%- endif %}
479+
460480
{% if cookiecutter.enable_redis == "True" -%}
461481
@pytest.fixture
462482
async def fake_redis_pool() -> AsyncGenerator[ConnectionPool, None]:
@@ -491,6 +511,9 @@ def fastapi_app(
491511
{%- if cookiecutter.enable_kafka == "True" %}
492512
test_kafka_producer: AIOKafkaProducer,
493513
{%- endif %}
514+
{%- if cookiecutter.enable_nats == "True" %}
515+
test_nats: Nats,
516+
{%- endif %}
494517
) -> FastAPI:
495518
"""
496519
Fixture for creating FastAPI app.
@@ -512,6 +535,9 @@ def fastapi_app(
512535
{%- if cookiecutter.enable_kafka == "True" %}
513536
application.dependency_overrides[get_kafka_producer] = lambda: test_kafka_producer
514537
{%- endif %}
538+
{%- if cookiecutter.enable_nats == "True" %}
539+
application.dependency_overrides[get_nats] = lambda: test_nats
540+
{%- endif %}
515541
return application # noqa: RET504
516542

517543

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import asyncio
2+
import uuid
3+
4+
from fastapi import FastAPI
5+
from httpx import AsyncClient
6+
from starlette import status
7+
from {{cookiecutter.project_name}}.settings import settings
8+
from natsrpy import Nats
9+
10+
11+
async def test_message_publishing(
12+
fastapi_app: FastAPI,
13+
client: AsyncClient,
14+
test_nats: Nats,
15+
) -> None:
16+
"""
17+
Test that messages are published correctly.
18+
19+
It sends message to kafka, reads it and
20+
validates that received message has the same
21+
value.
22+
23+
:param fastapi_app: current application.
24+
:param client: httpx client.
25+
"""
26+
subject = uuid.uuid4().hex
27+
payload = uuid.uuid4().hex
28+
29+
async with test_nats.subscribe(subject) as sub:
30+
{%- if cookiecutter.api_type == 'rest' %}
31+
url = fastapi_app.url_path_for("publish_nats_message")
32+
response = await client.post(
33+
url,
34+
json={
35+
"subject": subject,
36+
"message": payload,
37+
},
38+
)
39+
{%- elif cookiecutter.api_type == 'graphql' %}
40+
url = fastapi_app.url_path_for('handle_http_post')
41+
response = await client.post(
42+
url,
43+
json={
44+
"query": "mutation($message:NatsMessageDTO!)"
45+
"{publishNatsMessage(message:$message)}",
46+
"variables": {
47+
"message": {
48+
"subject": subject,
49+
"message": payload,
50+
},
51+
},
52+
},
53+
)
54+
{%- endif %}
55+
assert response.status_code == status.HTTP_200_OK
56+
message = await asyncio.wait_for(anext(sub), 1.0)
57+
assert message.payload == payload.encode()

fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def main() -> None:
7777
port=settings.port,
7878
reload=settings.reload,
7979
log_level=settings.log_level.value.lower(),
80+
access_log=True,
8081
factory=True,
8182
)
8283
{%- endif %}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from fastapi import Request
2+
from natsrpy import Nats
3+
4+
{%- if cookiecutter.enable_taskiq == "True" %}
5+
from taskiq import TaskiqDepends
6+
{%- endif %}
7+
8+
9+
def get_nats(request: Request {%- if cookiecutter.enable_taskiq == "True" %} = TaskiqDepends(){%- endif %}) -> Nats: # pragma: no cover
10+
"""
11+
Returns nats instance.
12+
13+
:param request: current request.
14+
:return: nats from the state.
15+
"""
16+
return request.app.state.nats

0 commit comments

Comments
 (0)