Template
mirror of
https://github.com/Nekidev/uv-fastapi-tortoise.git
synced 2026-09-12 19:57:25 +00:00
Add versioning support by default, replace _Project.md with _Summary.md and _Description.md, upgrade to the latest Tortoise ORM version
This commit is contained in:
+6
-11
@@ -2,26 +2,21 @@ from fastapi import FastAPI
|
||||
from fastapi.responses import ORJSONResponse
|
||||
|
||||
from project.lib import settings
|
||||
from project.api import docs
|
||||
from project.api.errors import BaseError, NotFoundError
|
||||
from project.api.lifespan import lifespan
|
||||
from project.api.example.router import router as example_router
|
||||
from project.api.v1 import app as app_v1
|
||||
from project.api.v1.errors import NotFoundError
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
debug=settings.DEBUG,
|
||||
title="Project API",
|
||||
description=docs.OPENAPI_DESCRIPTION,
|
||||
version="1.0.0",
|
||||
docs_url=None,
|
||||
redoc_url="/docs",
|
||||
redoc_url=None,
|
||||
lifespan=lifespan,
|
||||
openapi_tags=docs.OPENAPI_TAGS,
|
||||
default_response_class=ORJSONResponse,
|
||||
)
|
||||
|
||||
|
||||
app.include_router(example_router)
|
||||
|
||||
app.add_exception_handler(BaseError, BaseError.handler)
|
||||
app.add_exception_handler(404, NotFoundError.handler)
|
||||
|
||||
|
||||
app.mount("/v1", app_v1, name="v1")
|
||||
|
||||
+127
-17
@@ -1,27 +1,137 @@
|
||||
import os
|
||||
|
||||
from enum import Enum
|
||||
|
||||
OPENAPI_TAGS = []
|
||||
from dataclasses import dataclass
|
||||
|
||||
OPENAPI_DESCRIPTION = ""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
|
||||
for doc_file in os.listdir("./docs"):
|
||||
if not doc_file.endswith(".md"):
|
||||
continue
|
||||
@dataclass
|
||||
class OpenAPI:
|
||||
summary: str | None
|
||||
description: str | None
|
||||
tags: list["OpenAPITag"]
|
||||
|
||||
with open(f"./docs/{doc_file}", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
@classmethod
|
||||
def load(cls, base_path: str) -> "OpenAPI":
|
||||
"""Loads the OpenAPI documentation strings from a path.
|
||||
|
||||
name = doc_file.split(".")[0]
|
||||
Inside that path:
|
||||
_Summary.md -> The OpenAPI summary.
|
||||
_Description.md -> The OpenAPI description.
|
||||
*.md -> Tags, the file name without the prefix is the tag name and the file
|
||||
contents is the description.
|
||||
|
||||
if name == "_Project":
|
||||
OPENAPI_DESCRIPTION = content
|
||||
Args:
|
||||
base_path (str): The path to the directory where the files are.
|
||||
|
||||
else:
|
||||
OPENAPI_TAGS.append(
|
||||
{
|
||||
"name": name,
|
||||
"description": content,
|
||||
}
|
||||
)
|
||||
Returns:
|
||||
OpenAPI: The initialized OpenAPI object.
|
||||
"""
|
||||
|
||||
summary = None
|
||||
description = None
|
||||
tags = []
|
||||
|
||||
dirs = os.listdir(base_path)
|
||||
dirs.sort()
|
||||
|
||||
for doc_file in dirs:
|
||||
if not doc_file.endswith(".md"):
|
||||
continue
|
||||
|
||||
with open(f"{base_path}/{doc_file}", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
name = doc_file.split(".")[0]
|
||||
|
||||
if name == "_Summary":
|
||||
summary = content
|
||||
|
||||
elif name == "_Description":
|
||||
description = content
|
||||
|
||||
else:
|
||||
tags.append(OpenAPITag(name=name, description=content))
|
||||
|
||||
return cls(summary=summary, description=description, tags=tags)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpenAPITag:
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
def load(base_path: str) -> OpenAPI:
|
||||
"""Loads the OpenAPI documentation strings from a path.
|
||||
|
||||
Inside that path:
|
||||
_Summary.md -> The OpenAPI summary.
|
||||
_Description.md -> The OpenAPI description.
|
||||
*.md -> Tags, the file name without the prefix is the tag name and the file
|
||||
contents is the description.
|
||||
|
||||
Args:
|
||||
base_path (str): The path to the directory where the files are.
|
||||
|
||||
Returns:
|
||||
OpenAPI: The initialized OpenAPI object.
|
||||
"""
|
||||
|
||||
return OpenAPI.load(base_path)
|
||||
|
||||
|
||||
def load_onto_fastapi(
|
||||
base_path: str,
|
||||
app: FastAPI,
|
||||
*,
|
||||
only_tags: list[str | Enum] = None,
|
||||
only_used_tags: bool = True,
|
||||
) -> None:
|
||||
"""Loads the OpenAPI summaries and descriptions and loads them onto a `FastAPI`
|
||||
application.
|
||||
|
||||
Read `load()`'s documentation for information about structure.
|
||||
|
||||
Args:
|
||||
base_path (str): The path to the directory where the files are.
|
||||
app (FastAPI): The app to load the docs onto.
|
||||
only_tags (list[str | Enum], optional): A list of tags to load. If not
|
||||
provided, all tags will be loaded. Defaults to None.
|
||||
only_used_tags (bool, optional): If True, only tags that are used in the
|
||||
app's routes will be loaded. Defaults to True.
|
||||
"""
|
||||
|
||||
openapi = OpenAPI.load(base_path)
|
||||
|
||||
if only_tags is not None and only_used_tags:
|
||||
raise ValueError("Cannot use both `only_tags` and `only_used_tags`.")
|
||||
|
||||
if only_used_tags:
|
||||
used_tags = set()
|
||||
|
||||
for route in app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
for tag in route.tags:
|
||||
used_tags.add(tag)
|
||||
|
||||
openapi.tags = [tag for tag in openapi.tags if tag.name in used_tags]
|
||||
|
||||
elif only_tags is not None:
|
||||
only_tags = [tag.value if isinstance(tag, Enum) else tag for tag in only_tags]
|
||||
openapi.tags = [tag for tag in openapi.tags if tag.name in only_tags]
|
||||
|
||||
app.summary = openapi.summary
|
||||
app.description = openapi.description
|
||||
app.openapi_tags = []
|
||||
|
||||
for tag in openapi.tags:
|
||||
app.openapi_tags.append(
|
||||
{
|
||||
"name": tag.name,
|
||||
"description": tag.description,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,13 +2,13 @@ from fastapi import FastAPI
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from project.db.lifespan import on_startup, on_shutdown
|
||||
from project.db import lifespan as db
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await on_startup()
|
||||
await db.on_startup()
|
||||
|
||||
yield
|
||||
|
||||
await on_shutdown()
|
||||
await db.on_shutdown()
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import ORJSONResponse
|
||||
|
||||
from project.lib import settings
|
||||
from project.api import docs
|
||||
from project.api.v1.errors import BaseError, NotFoundError
|
||||
from project.api.v1.example.router import router as example_router
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
debug=settings.DEBUG,
|
||||
title="Project API",
|
||||
version="1.0.0",
|
||||
docs_url=None,
|
||||
redoc_url="/docs",
|
||||
default_response_class=ORJSONResponse,
|
||||
)
|
||||
|
||||
|
||||
app.include_router(example_router)
|
||||
|
||||
app.add_exception_handler(BaseError, BaseError.handler)
|
||||
app.add_exception_handler(404, NotFoundError.handler)
|
||||
|
||||
|
||||
docs.load_onto_fastapi("docs/v1", app)
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi.responses import ORJSONResponse
|
||||
|
||||
from project.api.schemas import ErrorSchema
|
||||
from project.api.v1.schemas import ErrorSchema
|
||||
|
||||
|
||||
class BaseError(Exception):
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from project.api.example.schemas import MessageSchema
|
||||
from project.api.v1.example.schemas import MessageSchema
|
||||
|
||||
|
||||
router = APIRouter(tags=["Example"])
|
||||
@@ -1,7 +1,11 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Query
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tortoise.queryset import QuerySet
|
||||
|
||||
|
||||
NanoID = Annotated[
|
||||
str, Field(min_length=21, max_length=21, pattern="^[A-Za-z0-9_-]{21}$")
|
||||
Reference in New Issue
Block a user