Skip to content

Commit

Permalink
Merge branch 'release/5.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
s3rius committed Sep 12, 2023
2 parents ddf31d4 + 6793473 commit 6eec110
Show file tree
Hide file tree
Showing 12 changed files with 272 additions and 10 deletions.
80 changes: 73 additions & 7 deletions fastapi_template/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def db_menu_update_info(ctx: BuilderContext, menu: SingularMenuModel) -> Builder
return ctx


def disable_orm(ctx: BuilderContext) -> MenuEntry:
def disable_orm(ctx: BuilderContext) -> Optional[MenuEntry]:
if ctx.db == "none":
ctx.orm = "none"
return SKIP_ENTRY
Expand All @@ -47,6 +47,12 @@ def do_not_ask_features_if_quite(ctx: BuilderContext) -> Optional[List[MenuEntry
return None


def do_not_ask_features_if_no_users(ctx: BuilderContext) -> Optional[list[MenuEntry]]:
if not ctx.add_users:
return [SKIP_ENTRY]
return None


def check_db(allowed_values: List[str]) -> Callable[[BuilderContext], bool]:
def checker(ctx: BuilderContext) -> bool:
return ctx.db not in allowed_values
Expand Down Expand Up @@ -74,7 +80,8 @@ def checker(ctx: BuilderContext) -> bool:
"Choose this option if you want to create a service with {name}.\n"
"It's more suitable for {generic} web-services or services without databases.".format(
name=colored("REST API", color="green"),
generic=colored("generic", color="cyan", attrs=["underline"]),
generic=colored("generic", color="cyan",
attrs=["underline"]),
)
),
),
Expand Down Expand Up @@ -145,7 +152,8 @@ def checker(ctx: BuilderContext) -> bool:
"{name} is the most popular database made by oracle.\n"
"It's a good fit for {prod} application.".format(
name=colored("MySQL", color="green"),
prod=colored("production-grade", color="cyan", attrs=["underline"]),
prod=colored("production-grade", color="cyan",
attrs=["underline"]),
)
),
additional_info=Database(
Expand All @@ -164,7 +172,8 @@ def checker(ctx: BuilderContext) -> bool:
"{name} is second most popular open-source relational database.\n"
"It's a good fit for {prod} application.".format(
name=colored("PostgreSQL", color="green"),
prod=colored("production-grade", color="cyan", attrs=["underline"]),
prod=colored("production-grade", color="cyan",
attrs=["underline"]),
)
),
additional_info=Database(
Expand Down Expand Up @@ -239,7 +248,8 @@ def checker(ctx: BuilderContext) -> bool:
"If you select this option, you will get only {what}.\n"
"The rest {warn}.".format(
what=colored("raw database", color="green"),
warn=colored("is up to you", color="red", attrs=["underline"]),
warn=colored("is up to you", color="red",
attrs=["underline"]),
)
),
),
Expand Down Expand Up @@ -330,7 +340,25 @@ def checker(ctx: BuilderContext) -> bool:
color="green",
),
purpose1=colored("caching", color="cyan"),
purpose2=colored("storing temporary variables", color="cyan"),
purpose2=colored(
"storing temporary variables", color="cyan"),
)
),
),
MenuEntry(
code="add_users",
cli_name="add_users",
user_view="Add fastapi-users support",
is_hidden=check_orm(["sqlalchemy"]),
description=(
"{name} is a user management extension.\n"
"Adds {purpose1} JWT or cookie endpoints and {purpose2} models CRUD's.".format(
name=colored(
"Fastapi-users",
color="cyan",
),
purpose1=colored("authentication", color="cyan"),
purpose2=colored("user", color="cyan"),
)
),
),
Expand Down Expand Up @@ -383,7 +411,8 @@ def checker(ctx: BuilderContext) -> bool:
"This option will add {what} manifests to your project.\n"
"But this option is {warn}, since if you want to use k8s, please create helm.".format(
what=colored("kubernetes", color="green"),
warn=colored("deprecated", color="red", attrs=["underline"]),
warn=colored("deprecated", color="red",
attrs=["underline"]),
)
),
),
Expand Down Expand Up @@ -534,6 +563,42 @@ def checker(ctx: BuilderContext) -> bool:
],
)

users_backend_menu = MultiselectMenuModel(
title="FastApi Users Backend",
code="users_menu",
description="Available backends for authentication with fastapi_users",
multiselect=True,
before_ask=do_not_ask_features_if_no_users,
entries=[
MenuEntry(
code="cookie_auth",
cli_name="cookie auth",
user_view="Add authentication via cookie support",
description=(
"Adds {cookie} authentication support.".format(
cookie=colored(
"cookie",
color="green",
)
)
),
),
MenuEntry(
code="jwt_auth",
cli_name="jwt auth",
user_view="Add JWT auth support",
description=(
"Adds {name} authentication support.".format(
name=colored(
"JWT",
color="green",
)
)
),
),
],
)


def handle_cli(
menus: List[BaseMenuModel],
Expand Down Expand Up @@ -575,6 +640,7 @@ def run_command(callback: Callable[[BuilderContext], None]) -> None:
orm_menu,
ci_menu,
features_menu,
users_backend_menu,
]

cmd = Command(
Expand Down
11 changes: 10 additions & 1 deletion fastapi_template/template/cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,20 @@
"gunicorn": {
"type": "bool"
},
"add_users": {
"type": "bool"
},
"cookie_auth": {
"type": "bool"
},
"jwt_auth": {
"type": "bool"
},
"_extensions": [
"cookiecutter.extensions.RandomStringExtension"
],
"_copy_without_render": [
"*.js",
"*.css"
]
}
}
3 changes: 3 additions & 0 deletions fastapi_template/template/{{cookiecutter.project_name}}/.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
{%- elif cookiecutter.db_info.name != 'none' %}
{{cookiecutter.project_name | upper}}_DB_HOST=localhost
{%- endif %}
{%- if cookiecutter.add_users == "True" %}
USERS_SECRET=""
{%- endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@
"{{cookiecutter.project_name}}/tests/test_rabbit.py"
]
},
"Users model": {
"enabled": "{{cookiecutter.add_users}}",
"resources": [
"{{cookiecutter.project_name}}/web/api/users",
"{{cookiecutter.project_name}}/db_sa/models/users.py"
]
},
"Dummy model": {
"enabled": "{{cookiecutter.add_dummy}}",
"resources": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ uvicorn = { version = "^0.22.0", extras = ["standard"] }
{%- if cookiecutter.gunicorn == "True" %}
gunicorn = "^21.2.0"
{%- endif %}
{%- if cookiecutter.add_users == "True" %}
{%- if cookiecutter.orm == "sqlalchemy" %}
fastapi-users = "^12.1.2"
httpx-oauth = "^0.10.2"
fastapi-users-db-sqlalchemy = "^6.0.1"
{%- endif %}
{%- endif %}
{%- if cookiecutter.pydanticv1 == "True" %}
pydantic = { version = "^1", extras=["dotenv"] }
{%- else %}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# type: ignore
import uuid

from fastapi import Depends
from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, schemas
from fastapi_users.authentication import (
AuthenticationBackend,
BearerTransport,

CookieTransport,
JWTStrategy,
)
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
from sqlalchemy.ext.asyncio import AsyncSession

from {{cookiecutter.project_name}}.db.base import Base
from {{cookiecutter.project_name}}.db.dependencies import get_db_session
from {{cookiecutter.project_name}}.settings import settings


class User(SQLAlchemyBaseUserTableUUID, Base):
"""Represents a user entity."""


class UserRead(schemas.BaseUser[uuid.UUID]):
"""Represents a read command for a user."""


class UserCreate(schemas.BaseUserCreate):
"""Represents a create command for a user."""


class UserUpdate(schemas.BaseUserUpdate):
"""Represents an update command for a user."""


class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
"""Manages a user session and its tokens."""
reset_password_token_secret = settings.users_secret
verification_token_secret = settings.users_secret


async def get_user_db(session: AsyncSession = Depends(get_db_session)) -> SQLAlchemyUserDatabase:
"""
Yield a SQLAlchemyUserDatabase instance.
:param session: asynchronous SQLAlchemy session.
:yields: instance of SQLAlchemyUserDatabase.
"""
yield SQLAlchemyUserDatabase(session, User)


async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)) -> UserManager:
"""
Yield a UserManager instance.
:param user_db: SQLAlchemy user db instance
:yields: an instance of UserManager.
"""
yield UserManager(user_db)


def get_jwt_strategy() -> JWTStrategy:
"""
Return a JWTStrategy in order to instantiate it dynamically.
:returns: instance of JWTStrategy with provided settings.
"""
return JWTStrategy(secret=settings.users_secret, lifetime_seconds=None)


{%- if cookiecutter.jwt_auth == "True" %}
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
auth_jwt = AuthenticationBackend(
name="jwt",
transport=bearer_transport,
get_strategy=get_jwt_strategy,
)
{%- endif %}

{%- if cookiecutter.cookie_auth == "True" %}
cookie_transport = CookieTransport()
auth_cookie = AuthenticationBackend(
name="cookie", transport=cookie_transport, get_strategy=get_jwt_strategy
)
{%- endif %}

backends = [
{%- if cookiecutter.cookie_auth == "True" %}
auth_cookie,
{%- endif %}
{%- if cookiecutter.jwt_auth == "True" %}
auth_jwt,
{%- endif %}
]

api_users = FastAPIUsers[User, uuid.UUID](get_user_manager, backends)

current_active_user = api_users.current_user(active=True)
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import enum
from pathlib import Path
from tempfile import gettempdir
Expand Down Expand Up @@ -43,9 +44,14 @@ class Settings(BaseSettings):

# Current environment
environment: str = "dev"

log_level: LogLevel = LogLevel.INFO

{%- if cookiecutter.add_users == "True" %}
{%- if cookiecutter.orm == "sqlalchemy" %}
users_secret: str = os.getenv("USERS_SECRET", "")
{%- endif %}
{%- endif %}
{% if cookiecutter.db_info.name != "none" -%}

# Variables for the database
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from fastapi.routing import APIRouter

{%- if cookiecutter.add_users == 'True' %}
from {{cookiecutter.project_name}}.web.api import users
from {{cookiecutter.project_name}}.db.models.users import api_users
{%- endif %}
{%- if cookiecutter.enable_routers == "True" %}
{%- if cookiecutter.api_type == 'rest' %}
from {{cookiecutter.project_name}}.web.api import echo
Expand Down Expand Up @@ -30,6 +34,9 @@

api_router = APIRouter()
api_router.include_router(monitoring.router)
{%- if cookiecutter.add_users == 'True' %}
api_router.include_router(users.router)
{%- endif %}
{%- if cookiecutter.self_hosted_swagger == "True" %}
api_router.include_router(docs.router)
{%- endif %}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""API for checking project status."""
from {{cookiecutter.project_name}}.web.api.users.views import router

__all__ = ["router"]

0 comments on commit 6eec110

Please sign in to comment.