Skip to content

Commit

Permalink
Merge branch 'development'
Browse files Browse the repository at this point in the history
  • Loading branch information
jonra1993 committed Dec 10, 2023
2 parents 863acdc + 5fbe141 commit 2a9bd83
Show file tree
Hide file tree
Showing 31 changed files with 2,317 additions and 1,971 deletions.
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ help:
@echo " Init database with sample data."
@echo " add-dev-migration"
@echo " Add new database migration using alembic."
@echo " upgrade-migration"
@echo " This helps to upgrade pending migrations."
@echo " run-pgadmin"
@echo " Run pgadmin4."
@echo " load-server-pgadmin"
Expand Down Expand Up @@ -123,6 +125,10 @@ add-dev-migration:
docker compose -f docker-compose-dev.yml exec fastapi_server alembic upgrade head && \
echo "Migration added and applied."

upgrade-migration:
docker compose -f docker-compose-dev.yml exec fastapi_server alembic upgrade head && \
echo "Migration upgraded."

run-pgadmin:
echo "$$SERVERS_JSON" > ./pgadmin/servers.json && \
docker volume create pgadmin_data && \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Async configuration for FastAPI and SQLModel

This is a project template which uses [FastAPI](https://fastapi.tiangolo.com/), [Alembic](https://alembic.sqlalchemy.org/en/latest/) and async [SQLModel](https://sqlmodel.tiangolo.com/) as ORM. It shows a complete async CRUD template using authentication. Our implementation utilizes the newest version of FastAPI and incorporates typing hints that are fully compatible with **Python 3.10** and later versions. If you're looking to build modern and efficient web applications with Python, this template will provide you with the necessary tools to get started quickly. You can read a short article with the motivations of starting this project in [Our Journey Using Async FastAPI](https://medium.com/allient/our-journey-using-async-fastapi-to-harnessing-the-power-of-modern-web-apis-90301827f14c?source=friends_link&sk=9006b3f2a4137a28a8576a69546c8c18). If you are looking for a started template CLI, I recommend you to use [create-fastapi-project](https://github.com/allient/create-fastapi-project)
This is a project template which uses [FastAPI](https://fastapi.tiangolo.com/), [Alembic](https://alembic.sqlalchemy.org/en/latest/) and async [SQLModel](https://sqlmodel.tiangolo.com/) as ORM which already is compatible with [Pydantic V2](https://docs.pydantic.dev/2.5/) and [SQLAlchemy V2.0](https://docs.sqlalchemy.org/en/20/). It shows a complete async CRUD template using authentication. Our implementation utilizes the newest version of FastAPI and incorporates typing hints that are fully compatible with **Python >=3.10**. If you're looking to build modern and efficient web applications with Python, this template will provide you with the necessary tools to get started quickly. You can read a short article with the motivations of starting this project in [Our Journey Using Async FastAPI](https://medium.com/allient/our-journey-using-async-fastapi-to-harnessing-the-power-of-modern-web-apis-90301827f14c?source=friends_link&sk=9006b3f2a4137a28a8576a69546c8c18). If you are looking for a started template CLI, I recommend you to use [create-fastapi-project](https://github.com/allient/create-fastapi-project)

## Why Use This Template?

Expand Down
10 changes: 5 additions & 5 deletions backend/app/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import with_statement
import asyncio
from logging.config import fileConfig
from sqlmodel import SQLModel, create_engine
from sqlmodel.ext.asyncio.session import AsyncEngine
from sqlmodel import SQLModel
from sqlalchemy.ext.asyncio import create_async_engine
from alembic import context
from app.core.config import Settings
import sys
Expand All @@ -24,6 +24,7 @@

target_metadata = SQLModel.metadata

db_url = str(settings.ASYNC_DATABASE_URI)
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
Expand All @@ -39,9 +40,8 @@ def run_migrations_offline():
Calls to context.execute() here emit the given string to the
script output.
"""
url = settings.ASYNC_DATABASE_URI
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True, dialect_opts={"paramstyle": "named"}
url=db_url, target_metadata=target_metadata, literal_binds=True, compare_type=True, dialect_opts={"paramstyle": "named"}
)

with context.begin_transaction():
Expand All @@ -59,7 +59,7 @@ async def run_migrations_online():
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = AsyncEngine(create_engine(settings.ASYNC_DATABASE_URI, echo=True, future=True))
connectable = create_async_engine(db_url, echo=True, future=True)

async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
Expand Down
2 changes: 2 additions & 0 deletions backend/app/app/api/v1/endpoints/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ async def login(
settings.REFRESH_TOKEN_EXPIRE_MINUTES,
)

print("data", data)
print("meta_data", meta_data)
return create_response(meta=meta_data, data=data, message="Login correctly")


Expand Down
1 change: 1 addition & 0 deletions backend/app/app/api/v1/endpoints/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ async def get_teams_list(
Gets a paginated list of teams
"""
teams = await crud.team.get_multi_paginated(params=params)
print("teams", teams)
return create_response(data=teams)


Expand Down
1 change: 1 addition & 0 deletions backend/app/app/api/v1/endpoints/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ async def upload_my_image(
file_data=BytesIO(image_modified.file_data),
content_type=image_file.content_type,
)
print("data_file", data_file)
media = IMediaCreate(
title=title, description=description, path=data_file.file_name
)
Expand Down
4 changes: 2 additions & 2 deletions backend/app/app/core/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
celery = Celery(
"async_task",
broker=f"redis://{settings.REDIS_HOST}:{settings.REDIS_PORT}",
backend=settings.SYNC_CELERY_DATABASE_URI,
backend=str(settings.SYNC_CELERY_DATABASE_URI),
include="app.api.celery_task", # route where tasks are defined
)

celery.conf.update({"beat_dburi": settings.SYNC_CELERY_BEAT_DATABASE_URI})
celery.conf.update({"beat_dburi": str(settings.SYNC_CELERY_BEAT_DATABASE_URI)})
celery.autodiscover_tasks()
134 changes: 70 additions & 64 deletions backend/app/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
from pydantic import BaseSettings, PostgresDsn, validator, EmailStr, AnyHttpUrl
from pydantic_core.core_schema import FieldValidationInfo
from pydantic import PostgresDsn, EmailStr, AnyHttpUrl, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Any
import secrets
from enum import Enum
Expand All @@ -22,79 +24,83 @@ class Settings(BaseSettings):
DATABASE_USER: str
DATABASE_PASSWORD: str
DATABASE_HOST: str
DATABASE_PORT: int | str
DATABASE_PORT: int
DATABASE_NAME: str
DATABASE_CELERY_NAME: str = "celery_schedule_jobs"
REDIS_HOST: str
REDIS_PORT: str
DB_POOL_SIZE = 83
WEB_CONCURRENCY = 9
POOL_SIZE = max(DB_POOL_SIZE // WEB_CONCURRENCY, 5)
ASYNC_DATABASE_URI: PostgresDsn | None
DB_POOL_SIZE: int = 83
WEB_CONCURRENCY: int = 9
POOL_SIZE: int = max(DB_POOL_SIZE // WEB_CONCURRENCY, 5)
ASYNC_DATABASE_URI: PostgresDsn | str = ""

@validator("ASYNC_DATABASE_URI", pre=True)
def assemble_db_connection(cls, v: str | None, values: dict[str, Any]) -> Any:
@field_validator("ASYNC_DATABASE_URI", mode="after")
def assemble_db_connection(cls, v: str | None, info: FieldValidationInfo) -> Any:
if isinstance(v, str):
return v
return PostgresDsn.build(
scheme="postgresql+asyncpg",
user=values.get("DATABASE_USER"),
password=values.get("DATABASE_PASSWORD"),
host=values.get("DATABASE_HOST"),
port=str(values.get("DATABASE_PORT")),
path=f"/{values.get('DATABASE_NAME') or ''}",
)

SYNC_CELERY_DATABASE_URI: str | None

@validator("SYNC_CELERY_DATABASE_URI", pre=True)
if v == "":
return PostgresDsn.build(
scheme="postgresql+asyncpg",
username=info.data["DATABASE_USER"],
password=info.data["DATABASE_PASSWORD"],
host=info.data["DATABASE_HOST"],
port=info.data["DATABASE_PORT"],
path=info.data["DATABASE_NAME"],
)
return v

SYNC_CELERY_DATABASE_URI: PostgresDsn | str = ""

@field_validator("SYNC_CELERY_DATABASE_URI", mode="after")
def assemble_celery_db_connection(
cls, v: str | None, values: dict[str, Any]
cls, v: str | None, info: FieldValidationInfo
) -> Any:
if isinstance(v, str):
return v
return PostgresDsn.build(
scheme="db+postgresql",
user=values.get("DATABASE_USER"),
password=values.get("DATABASE_PASSWORD"),
host=values.get("DATABASE_HOST"),
port=str(values.get("DATABASE_PORT")),
path=f"/{values.get('DATABASE_CELERY_NAME') or ''}",
)

SYNC_CELERY_BEAT_DATABASE_URI: str | None

@validator("SYNC_CELERY_BEAT_DATABASE_URI", pre=True)
if v == "":
return PostgresDsn.build(
scheme="db+postgresql",
username=info.data["DATABASE_USER"],
password=info.data["DATABASE_PASSWORD"],
host=info.data["DATABASE_HOST"],
port=info.data["DATABASE_PORT"],
path=info.data["DATABASE_CELERY_NAME"],
)
return v

SYNC_CELERY_BEAT_DATABASE_URI: PostgresDsn | str = ""

@field_validator("SYNC_CELERY_BEAT_DATABASE_URI", mode="after")
def assemble_celery_beat_db_connection(
cls, v: str | None, values: dict[str, Any]
cls, v: str | None, info: FieldValidationInfo
) -> Any:
if isinstance(v, str):
return v
return PostgresDsn.build(
scheme="postgresql+psycopg2",
user=values.get("DATABASE_USER"),
password=values.get("DATABASE_PASSWORD"),
host=values.get("DATABASE_HOST"),
port=str(values.get("DATABASE_PORT")),
path=f"/{values.get('DATABASE_CELERY_NAME') or ''}",
)

ASYNC_CELERY_BEAT_DATABASE_URI: str | None

@validator("ASYNC_CELERY_BEAT_DATABASE_URI", pre=True)
if v == "":
return PostgresDsn.build(
scheme="postgresql+psycopg2",
username=info.data["DATABASE_USER"],
password=info.data["DATABASE_PASSWORD"],
host=info.data["DATABASE_HOST"],
port=info.data["DATABASE_PORT"],
path=info.data["DATABASE_CELERY_NAME"],
)
return v

ASYNC_CELERY_BEAT_DATABASE_URI: PostgresDsn | str = ""

@field_validator("ASYNC_CELERY_BEAT_DATABASE_URI", mode="after")
def assemble_async_celery_beat_db_connection(
cls, v: str | None, values: dict[str, Any]
cls, v: str | None, info: FieldValidationInfo
) -> Any:
if isinstance(v, str):
return v
return PostgresDsn.build(
scheme="postgresql+asyncpg",
user=values.get("DATABASE_USER"),
password=values.get("DATABASE_PASSWORD"),
host=values.get("DATABASE_HOST"),
port=str(values.get("DATABASE_PORT")),
path=f"/{values.get('DATABASE_CELERY_NAME') or ''}",
)
if v == "":
return PostgresDsn.build(
scheme="postgresql+asyncpg",
username=info.data["DATABASE_USER"],
password=info.data["DATABASE_PASSWORD"],
host=info.data["DATABASE_HOST"],
port=info.data["DATABASE_PORT"],
path=info.data["DATABASE_CELERY_NAME"],
)
return v

FIRST_SUPERUSER_EMAIL: EmailStr
FIRST_SUPERUSER_PASSWORD: str
Expand All @@ -107,20 +113,20 @@ def assemble_async_celery_beat_db_connection(
WHEATER_URL: AnyHttpUrl

SECRET_KEY: str = secrets.token_urlsafe(32)
ENCRYPT_KEY = secrets.token_urlsafe(32)
ENCRYPT_KEY: str = secrets.token_urlsafe(32)
BACKEND_CORS_ORIGINS: list[str] | list[AnyHttpUrl]

@validator("BACKEND_CORS_ORIGINS", pre=True)
@field_validator("BACKEND_CORS_ORIGINS")
def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str:
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
elif isinstance(v, (list, str)):
return v
raise ValueError(v)

class Config:
case_sensitive = True
env_file = os.path.expanduser("~/.env")
model_config = SettingsConfigDict(
case_sensitive=True, env_file=os.path.expanduser("~/.env")
)


settings = Settings()
10 changes: 6 additions & 4 deletions backend/app/app/crud/base_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
from typing import Any, Generic, TypeVar
from uuid import UUID
from app.schemas.common_schema import IOrderEnum
from fastapi_pagination.ext.async_sqlalchemy import paginate
from fastapi_pagination.ext.sqlmodel import paginate
from fastapi_async_sqlalchemy import db
from fastapi_async_sqlalchemy.middleware import DBSessionMeta
from fastapi_pagination import Params, Page
from pydantic import BaseModel
from sqlmodel import SQLModel, select, func
Expand All @@ -30,7 +29,7 @@ def __init__(self, model: type[ModelType]):
self.model = model
self.db = db

def get_db(self) -> DBSessionMeta:
def get_db(self) -> type(db):
return self.db

async def get(
Expand Down Expand Up @@ -86,7 +85,10 @@ async def get_multi_paginated(
db_session = db_session or self.db.session
if query is None:
query = select(self.model)
return await paginate(db_session, query, params)

output = await paginate(db_session, query, params)
print("output", output)
return output

async def get_multi_paginated_ordered(
self,
Expand Down
8 changes: 4 additions & 4 deletions backend/app/app/db/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
connect_args = {"check_same_thread": False}

engine = create_async_engine(
settings.ASYNC_DATABASE_URI,
str(settings.ASYNC_DATABASE_URI),
echo=False,
future=True,
# pool_size=POOL_SIZE,
Expand All @@ -31,11 +31,11 @@
)

engine_celery = create_async_engine(
settings.ASYNC_CELERY_BEAT_DATABASE_URI,
str(settings.ASYNC_CELERY_BEAT_DATABASE_URI),
# echo=True,
future=True,
pool_size=POOL_SIZE,
max_overflow=64,
#pool_size=POOL_SIZE,
#max_overflow=64,
)

SessionLocalCelery = sessionmaker(
Expand Down
2 changes: 1 addition & 1 deletion backend/app/app/deps/celery_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
def get_job_db() -> Generator:
session_manager = SessionManager()
engine, _session = session_manager.create_session(
settings.SYNC_CELERY_BEAT_DATABASE_URI
str(settings.SYNC_CELERY_BEAT_DATABASE_URI)
)

with _session() as session:
Expand Down
5 changes: 2 additions & 3 deletions backend/app/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ async def lifespan(app: FastAPI):

app.add_middleware(
SQLAlchemyMiddleware,
db_url=settings.ASYNC_DATABASE_URI,
db_url=str(settings.ASYNC_DATABASE_URI),
engine_args={
"echo": False,
# "pool_pre_ping": True,
Expand Down Expand Up @@ -237,5 +237,4 @@ async def websocket_endpoint(websocket: WebSocket, user_id: UUID):


# Add Routers
app.include_router(api_router_v1, prefix=settings.API_V1_STR)
add_pagination(app)
app.include_router(api_router_v1, prefix=settings.API_V1_STR)
5 changes: 2 additions & 3 deletions backend/app/app/models/base_uuid_model.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from app.utils.uuid6 import uuid7, UUID
from uuid import UUID
from app.utils.uuid6 import uuid7
from sqlmodel import SQLModel as _SQLModel, Field
from sqlalchemy.orm import declared_attr
from datetime import datetime

# id: implements proposal uuid7 draft4


class SQLModel(_SQLModel):
@declared_attr # type: ignore
def __tablename__(cls) -> str:
Expand Down
6 changes: 3 additions & 3 deletions backend/app/app/models/image_media_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@


class ImageMediaBase(SQLModel):
file_format: str | None
width: int | None
height: int | None
file_format: str | None = None
width: int | None = None
height: int | None = None


class ImageMedia(BaseUUIDModel, ImageMediaBase, table=True):
Expand Down
Loading

0 comments on commit 2a9bd83

Please sign in to comment.