Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: MongoDB support via beanie #187

Merged
merged 21 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion fastapi_template/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,23 @@ def checker(ctx: BuilderContext) -> bool:
port=5432,
),
),
MenuEntry(
code="mongodb",
user_view="MongoDB",
description=(
"{name} is one of the most popular NoSQL databases out there.".format(
name=colored("MongoDB", color="green"),
)
),
additional_info=Database(
name="mongodb",
image="mongo:4.2",
async_driver="beanie",
driver_short="pymongo",
driver="pymongo",
port=27017
),
)
],
)

Expand Down Expand Up @@ -234,7 +251,7 @@ def checker(ctx: BuilderContext) -> bool:
entries=[
MenuEntry(
code="none",
user_view="Whithout ORMs",
user_view="Without ORMs",
description=(
"If you select this option, you will get only {what}.\n"
"The rest {warn}.".format(
Expand All @@ -246,6 +263,7 @@ def checker(ctx: BuilderContext) -> bool:
MenuEntry(
code="ormar",
user_view="Ormar",
is_hidden=check_db(["sqlite", "mysql", "postgresql"]),
pydantic_v1=True,
description=(
"{what} is a great {feature} ORM.\n"
Expand All @@ -258,6 +276,7 @@ def checker(ctx: BuilderContext) -> bool:
MenuEntry(
code="sqlalchemy",
user_view="SQLAlchemy",
is_hidden=check_db(["sqlite", "mysql", "postgresql"]),
description=(
"{what} is the most popular python ORM.\n"
"It has a {feature} and a big community around it.".format(
Expand All @@ -269,6 +288,7 @@ def checker(ctx: BuilderContext) -> bool:
MenuEntry(
code="tortoise",
user_view="Tortoise",
is_hidden=check_db(["sqlite", "mysql", "postgresql"]),
description=(
"{what} is a great {feature} ORM.\n"
"It's easy to use, it has it's own migration tooling.".format(
Expand Down Expand Up @@ -302,6 +322,18 @@ def checker(ctx: BuilderContext) -> bool:
)
),
),
MenuEntry(
code="beanie",
user_view="Beanie",
is_hidden=check_db(["mongodb"]),
description=(
"{what} is an asynchronous object-document mapper (ODM) for MongoDB.\n"
"Data models are based on Pydantic.".format(
what=colored("Beanie", color="green"),
)
),
),

],
)

Expand Down
11 changes: 11 additions & 0 deletions fastapi_template/template/{{cookiecutter.project_name}}/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ $ tree "{{cookiecutter.project_name}}"
├── conftest.py # Fixtures for all tests.
{%- if cookiecutter.db_info.name != "none" %}
├── db # module contains db configurations
{%- if cookiecutter.db_info.name != "mongodb" %}
│   ├── dao # Data Access Objects. Contains different classes to interact with database.
{%- endif %}
│   └── models # Package contains different models for ORMs.
{%- endif %}
├── __main__.py # Startup script. Starts uvicorn.
Expand Down Expand Up @@ -130,6 +132,15 @@ By default it runs:

You can read more about pre-commit here: https://pre-commit.com/

{%- if cookiecutter.db_info.name == 'mongodb' %}
## MongoDB

Start a MongoDB container via docker compose by:
```shell
docker-compose -f deploy/docker-compose.yml --project-directory . up -d db
```
{%- endif %}

{%- if cookiecutter.enable_kube == 'True' %}

## Kubernetes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,24 @@
"alembic.ini",
"{{cookiecutter.project_name}}/web/api/dummy",
"{{cookiecutter.project_name}}/web/gql/dummy",
"{{cookiecutter.project_name}}/db_sa",
usefulalgorithm marked this conversation as resolved.
Show resolved Hide resolved
"{{cookiecutter.project_name}}/tests/test_dummy.py",
"deploy/kube/db.yml"
]
},
"SQLAlchemy support": {
"enabled": "{{cookiecutter.db_info.name not in ['none', 'mongodb']}}",
usefulalgorithm marked this conversation as resolved.
Show resolved Hide resolved
"resources": [
"{{cookiecutter.project_name}}/db_sa"
]
},
"Beanie support": {
"enabled": "{{cookiecutter.db_info.name == 'mongodb'}}",
"resources": [
"{{cookiecutter.project_name}}/db_beanie"
]
},
"Postgres and MySQL support": {
"enabled": "{{cookiecutter.db_info.name != 'sqlite'}}",
"enabled": "{{cookiecutter.db_info.name not in ['sqlite', 'mongodb']}}",
"resources": [
"deploy/kube/db.yml"
]
Expand Down Expand Up @@ -136,6 +147,7 @@
"{{cookiecutter.project_name}}/tests/test_dummy.py",
"{{cookiecutter.project_name}}/db_piccolo/dao",
"{{cookiecutter.project_name}}/db_piccolo/models/dummy_model.py",
"{{cookiecutter.project_name}}/db_beanie/models/dummy_model.py",
"{{cookiecutter.project_name}}/db_sa/migrations/versions/2021-08-16-16-55_2b7380507a71.py",
"{{cookiecutter.project_name}}/db_ormar/migrations/versions/2021-08-16-16-55_2b7380507a71.py",
"{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_pg.sql",
Expand Down Expand Up @@ -182,6 +194,12 @@
"{{cookiecutter.project_name}}/piccolo_conf.py"
]
},
"Beanie": {
"enabled": "{{cookiecutter.orm == 'beanie'}}",
"resources": [
"{{cookiecutter.project_name}}/db_beanie"
]
},
"Postgresql DB": {
"enabled": "{{cookiecutter.db_info.name == 'postgresql'}}",
"resources": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ services:
# Exposes application port.
- "8000:8000"
build:
context: .
target: dev
volumes:
# Adds current directory as volume.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ services:
retries: 40
{%- endif %}

{%- if cookiecutter.db_info.name == "mongodb"%}
db:
image: {{cookiecutter.db_info.image}}
ports:
usefulalgorithm marked this conversation as resolved.
Show resolved Hide resolved
- "{{cookiecutter.db_info.port}}:{{cookiecutter.db_info.port}}"
restart: always
usefulalgorithm marked this conversation as resolved.
Show resolved Hide resolved
environment:
MONGO_INITDB_ROOT_USERNAME: "{{cookiecutter.project_name}}"
MONGO_INITDB_ROOT_PASSWORD: "{{cookiecutter.project_name}}"
command: "mongod"
{%- endif %}

{%- if cookiecutter.db_info.name == "mysql" %}

db:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ aiofiles = "^23.1.0"
psycopg = { version = "^3.1.9", extras = ["binary", "pool"] }
{%- endif %}
httptools = "^0.6.0"
{%- if cookiecutter.orm == "beanie" %}
beanie = "^1.21.0"
{%- else %}
pymongo = "^4.5.0"
{%- endif %}
{%- if cookiecutter.api_type == "graphql" %}
strawberry-graphql = { version = "^0.194.4", extras = ["fastapi"] }
{%- endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"{{cookiecutter.project_name}}/db_ormar",
"{{cookiecutter.project_name}}/db_tortoise",
"{{cookiecutter.project_name}}/db_psycopg",
"{{cookiecutter.project_name}}/db_piccolo"
"{{cookiecutter.project_name}}/db_piccolo",
"{{cookiecutter.project_name}}/db_beanie"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""{{cookiecutter.project_name}} models."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from beanie import Document

class DummyModel(Document):
"""Model for demo purpose."""
name: str
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ class Settings(BaseSettings):
db_port: int = {{cookiecutter.db_info.port}}
db_user: str = "{{cookiecutter.project_name}}"
db_pass: str = "{{cookiecutter.project_name}}"
{%- if cookiecutter.db_info.name != "sqlite" %}
db_base: str = "admin"
{%- else %}
db_base: str = "{{cookiecutter.project_name}}"
{%- endif %}
{%- endif %}
db_echo: bool = False

{%- endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@

{%- endif %}
from starlette import status
{%- if cookiecutter.orm != 'beanie' %}
from {{cookiecutter.project_name}}.db.dao.dummy_dao import DummyDAO

{%- endif %}
from {{cookiecutter.project_name}}.db.models.dummy_model import DummyModel


Expand Down Expand Up @@ -56,8 +59,14 @@ async def test_creation(
{%- elif cookiecutter.orm in ["tortoise", "ormar", "piccolo"] %}
dao = DummyDAO()
{%- endif %}

{%- if cookiecutter.orm == "beanie" %}
instance = await DummyModel.find(DummyModel.name == test_name).first_or_none()
assert instance is not None and instance.name == test_name
{%- else %}
instances = await dao.filter(name=test_name)
assert instances[0].name == test_name
{%- endif %}


@pytest.mark.anyio
Expand All @@ -79,7 +88,11 @@ async def test_getting(
dao = DummyDAO()
{%- endif %}
test_name = uuid.uuid4().hex
{%- if cookiecutter.orm == "beanie" %}
await DummyModel.insert_one(DummyModel(name=test_name))
{%- else %}
await dao.create_dummy_model(name=test_name)
{%- endif %}

{%- if cookiecutter.api_type == 'rest' %}
url = fastapi_app.url_path_for('get_dummy_models')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
from pydantic import ConfigDict

{%- endif %}
{%- if cookiecutter.db_info.name == "mongodb" %}
from pydantic import field_validator
from bson import ObjectId
{%- endif %}


class DummyModelDTO(BaseModel):
Expand All @@ -13,9 +17,24 @@ class DummyModelDTO(BaseModel):
It returned when accessing dummy models from the API.
"""

{%- if cookiecutter.db_info.name != "mongodb" %}
id: int
{%- else %}
id: str
{%- endif %}
name: str

{%- if cookiecutter.db_info.name == "mongodb" %}
@field_validator("id", mode="before")
usefulalgorithm marked this conversation as resolved.
Show resolved Hide resolved
@classmethod
def parse_object_id(cls, document_id: ObjectId) -> str:
"""
Validator for turning an incoming `ObjectId` into a
json serializable `str`.
"""
return str(document_id)
{%- endif %}


{%- if cookiecutter.pydanticv1 == "True" %}
class Config:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from fastapi import APIRouter
from fastapi.param_functions import Depends
{%- if cookiecutter.db_info.name != "mongodb" %}
from {{cookiecutter.project_name}}.db.dao.dummy_dao import DummyDAO
{%- endif %}
from {{cookiecutter.project_name}}.db.models.dummy_model import DummyModel
from {{cookiecutter.project_name}}.web.api.dummy.schema import (DummyModelDTO,
DummyModelInputDTO)
Expand All @@ -14,28 +16,44 @@
async def get_dummy_models(
limit: int = 10,
offset: int = 0,
{%- if cookiecutter.db_info.name != "mongodb" %}
dummy_dao: DummyDAO = Depends(),
{%- endif %}
) -> List[DummyModel]:
"""
Retrieve all dummy objects from the database.

:param limit: limit of dummy objects, defaults to 10.
:param offset: offset of dummy objects, defaults to 0.
{%- if cookiecutter.db_info.name != "mongodb" %}
:param dummy_dao: DAO for dummy models.
{%- endif %}
:return: list of dummy objects from database.
"""
{%- if cookiecutter.db_info.name != "mongodb" %}
return await dummy_dao.get_all_dummies(limit=limit, offset=offset)
{%- else %}
return await DummyModel.find_all(skip=offset, limit=limit).to_list()
{%- endif %}


@router.put("/")
async def create_dummy_model(
new_dummy_object: DummyModelInputDTO,
{%- if cookiecutter.db_info.name != "mongodb" %}
dummy_dao: DummyDAO = Depends(),
{%- endif %}
) -> None:
"""
Creates dummy model in the database.

:param new_dummy_object: new dummy model item.
{%- if cookiecutter.db_info.name != "mongodb" %}
:param dummy_dao: DAO for dummy models.
{%- endif %}
"""
{%- if cookiecutter.db_info.name != "mongodb" %}
await dummy_dao.create_dummy_model(name=new_dummy_object.name)
{%- else %}
await DummyModel.insert_one(DummyModel(name=new_dummy_object.name))
usefulalgorithm marked this conversation as resolved.
Show resolved Hide resolved
{%- endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,24 @@ def _setup_db(app: FastAPI) -> None: # pragma: no cover
app.state.db_session_factory = session_factory
{%- endif %}

{%- if cookiecutter.orm == "beanie" %}
import beanie
from motor.motor_asyncio import AsyncIOMotorClient
from {{cookiecutter.project_name}}.db.models.dummy_model import DummyModel
async def _setup_db(app: FastAPI) -> None:
client = AsyncIOMotorClient(
f"mongodb://{settings.db_user}:{settings.db_pass}"
usefulalgorithm marked this conversation as resolved.
Show resolved Hide resolved
+ f"@{settings.db_host}:{settings.db_port}/{settings.db_base}"
)
app.state.db_client = client
await beanie.init_beanie(
database=client[settings.db_base],
document_models=[
DummyModel, # type: ignore
]
)
{%- endif %}

{%- if cookiecutter.enable_migrations != "True" %}
{%- if cookiecutter.orm in ["ormar", "sqlalchemy"] %}
async def _create_tables() -> None: # pragma: no cover
Expand Down Expand Up @@ -276,7 +294,7 @@ async def _startup() -> None: # noqa: WPS430
_setup_db(app)
{%- elif cookiecutter.orm == "ormar" %}
await database.connect()
{%- elif cookiecutter.orm == "psycopg" %}
{%- elif cookiecutter.orm in ["beanie", "psycopg"] %}
await _setup_db(app)
{%- endif %}
{%- if cookiecutter.db_info.name != "none" and cookiecutter.enable_migrations != "True" %}
Expand Down
Loading