diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f2d22e..e7485eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,18 +2,17 @@ exclude: 'docs|node_modules|migrations|.git|.tox' repos: - repo: https://github.com/ambv/black - rev: 22.12.0 + rev: 23.3.0 hooks: - id: black language_version: python3.11 - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.216' + rev: 'v0.0.265' hooks: - id: ruff - args: ["--force-exclude"] - repo: https://github.com/crate-ci/typos - rev: v1.13.6 + rev: typos-dict-v0.9.26 hooks: - id: typos diff --git a/fastapi_book_server/app/services/author.py b/fastapi_book_server/app/services/author.py index 422373e..5e3aa5c 100644 --- a/fastapi_book_server/app/services/author.py +++ b/fastapi_book_server/app/services/author.py @@ -3,6 +3,7 @@ from typing import TypedDict from app.models import Author from app.services.common import GetRandomService, MeiliSearchService, TRGMSearchService + GET_OBJECT_IDS_QUERY = """ SELECT ARRAY( WITH filtered_authors AS ( diff --git a/fastapi_book_server/app/services/book.py b/fastapi_book_server/app/services/book.py index 96c451a..0b0007a 100644 --- a/fastapi_book_server/app/services/book.py +++ b/fastapi_book_server/app/services/book.py @@ -8,6 +8,7 @@ from app.services.common import ( TRGMSearchService, ) + GET_OBJECT_IDS_QUERY = """ SELECT ARRAY( WITH filtered_books AS ( diff --git a/fastapi_book_server/app/services/common.py b/fastapi_book_server/app/services/common.py index 7c7c37a..c0c52c6 100644 --- a/fastapi_book_server/app/services/common.py +++ b/fastapi_book_server/app/services/common.py @@ -15,9 +15,10 @@ from redis import asyncio as aioredis from sqlalchemy import Table from app.utils.orjson_default import default as orjson_default -from app.utils.pagination import CustomPage, Page +from app.utils.pagination import Page from core.config import env_config + MODEL = TypeVar("MODEL", bound=Model) QUERY = TypeVar("QUERY", bound=TypedDict) @@ -170,7 +171,7 @@ class BaseSearchService(Generic[MODEL, QUERY], BaseService[MODEL, QUERY]): total, objects = await cls.get_limited_objects(query, redis, no_cache) - return CustomPage.create(items=objects, total=total, params=params) + return Page.create(items=objects, total=total, params=params) class SearchQuery(TypedDict): diff --git a/fastapi_book_server/app/services/sequence.py b/fastapi_book_server/app/services/sequence.py index 6f36ec4..9d1e83d 100644 --- a/fastapi_book_server/app/services/sequence.py +++ b/fastapi_book_server/app/services/sequence.py @@ -3,6 +3,7 @@ from typing import TypedDict from app.models import Sequence from app.services.common import GetRandomService, MeiliSearchService, TRGMSearchService + GET_OBJECT_IDS_QUERY = """ SELECT ARRAY ( WITH filtered_sequences AS ( diff --git a/fastapi_book_server/app/services/translator.py b/fastapi_book_server/app/services/translator.py index 0e054ea..0e12821 100644 --- a/fastapi_book_server/app/services/translator.py +++ b/fastapi_book_server/app/services/translator.py @@ -1,6 +1,7 @@ from app.models import Author from app.services.common import MeiliSearchService, TRGMSearchService + GET_OBJECT_IDS_QUERY = """ SELECT ARRAY( WITH filtered_authors AS ( diff --git a/fastapi_book_server/app/utils/orjson_default.py b/fastapi_book_server/app/utils/orjson_default.py index 2fc3efe..c7deccb 100644 --- a/fastapi_book_server/app/utils/orjson_default.py +++ b/fastapi_book_server/app/utils/orjson_default.py @@ -1,5 +1,7 @@ from typing import Any +import orjson + def default(value: Any): if isinstance(value, frozenset): @@ -7,3 +9,7 @@ def default(value: Any): return "-".join(sorted(list_value)) return value + + +def orjson_dumps(v, *, default) -> str: + return orjson.dumps(v, default=default).decode() diff --git a/fastapi_book_server/app/utils/pagination.py b/fastapi_book_server/app/utils/pagination.py index f035d37..9163576 100644 --- a/fastapi_book_server/app/utils/pagination.py +++ b/fastapi_book_server/app/utils/pagination.py @@ -1,8 +1,18 @@ -from typing import Any, Generic, Protocol, Sequence, TypeVar, runtime_checkable +from math import ceil +from typing import ( + Any, + Generic, + Protocol, + Sequence, + TypeVar, + runtime_checkable, +) -from fastapi_pagination import Page, Params -from fastapi_pagination.bases import AbstractParams -from pydantic import conint +from fastapi_pagination import Params +from fastapi_pagination.bases import AbstractParams, BasePage +from fastapi_pagination.types import GreaterEqualOne, GreaterEqualZero +import orjson +from utils.orjson_default import orjson_dumps @runtime_checkable @@ -14,23 +24,36 @@ class ToDict(Protocol): T = TypeVar("T", ToDict, Any) -class CustomPage(Page[T], Generic[T]): - total_pages: conint(ge=0) # type: ignore +class Page(BasePage[T], Generic[T]): + page: GreaterEqualOne + size: GreaterEqualOne + total_pages: GreaterEqualZero + + __params_type__ = Params + + class Config: + json_loads = orjson.loads + json_dumps = orjson_dumps @classmethod def create( cls, items: Sequence[T], - total: int, params: AbstractParams, - ) -> Page[T]: + *, + total: int, + **kwargs: Any, + ) -> "Page[T]": if not isinstance(params, Params): raise ValueError("Page should be used with Params") + pages = ceil(total / params.size) + return cls( total=total, - items=[item.dict() for item in items], + items=items, page=params.page, size=params.size, - total_pages=(total + params.size - 1) // params.size, + total_pages=pages, + **kwargs, ) diff --git a/fastapi_book_server/app/views/__init__.py b/fastapi_book_server/app/views/__init__.py index 689dbfe..c8067a8 100644 --- a/fastapi_book_server/app/views/__init__.py +++ b/fastapi_book_server/app/views/__init__.py @@ -8,6 +8,7 @@ from app.views.sequence import sequence_router from app.views.source import source_router from app.views.translation import translation_router + routers = [ source_router, author_router, diff --git a/fastapi_book_server/app/views/author.py b/fastapi_book_server/app/views/author.py index 8d20fc6..8020882 100644 --- a/fastapi_book_server/app/views/author.py +++ b/fastapi_book_server/app/views/author.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status + from fastapi_pagination import Params from fastapi_pagination.ext.ormar import paginate @@ -10,7 +11,8 @@ from app.serializers.author import Author, AuthorBook, TranslatedBook from app.serializers.author_annotation import AuthorAnnotation from app.services.author import AuthorMeiliSearchService, GetRandomAuthorService from app.services.translator import TranslatorMeiliSearchService -from app.utils.pagination import CustomPage +from app.utils.pagination import Page + author_router = APIRouter( prefix="/api/v1/authors", @@ -23,9 +25,7 @@ PREFETCH_RELATED_FIELDS = ["source"] SELECT_RELATED_FIELDS = ["annotations"] -@author_router.get( - "/", response_model=CustomPage[Author], dependencies=[Depends(Params)] -) +@author_router.get("/", response_model=Page[Author], dependencies=[Depends(Params)]) async def get_authors(): return await paginate( AuthorDB.objects.select_related(SELECT_RELATED_FIELDS).prefetch_related( @@ -75,7 +75,7 @@ async def get_author_annotation(id: int): @author_router.get( - "/{id}/books", response_model=CustomPage[AuthorBook], dependencies=[Depends(Params)] + "/{id}/books", response_model=Page[AuthorBook], dependencies=[Depends(Params)] ) async def get_author_books( id: int, allowed_langs: list[str] = Depends(get_allowed_langs) @@ -89,7 +89,7 @@ async def get_author_books( @author_router.get( - "/search/{query}", response_model=CustomPage[Author], dependencies=[Depends(Params)] + "/search/{query}", response_model=Page[Author], dependencies=[Depends(Params)] ) async def search_authors( query: str, @@ -109,7 +109,7 @@ translator_router = APIRouter( ) -@translator_router.get("/{id}/books", response_model=CustomPage[TranslatedBook]) +@translator_router.get("/{id}/books", response_model=Page[TranslatedBook]) async def get_translated_books( id: int, allowed_langs: list[str] = Depends(get_allowed_langs) ): @@ -125,7 +125,7 @@ async def get_translated_books( @translator_router.get( - "/search/{query}", response_model=CustomPage[Author], dependencies=[Depends(Params)] + "/search/{query}", response_model=Page[Author], dependencies=[Depends(Params)] ) async def search_translators( query: str, diff --git a/fastapi_book_server/app/views/author_annotation.py b/fastapi_book_server/app/views/author_annotation.py index c8379f0..e8c7bf5 100644 --- a/fastapi_book_server/app/views/author_annotation.py +++ b/fastapi_book_server/app/views/author_annotation.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, status + from fastapi_pagination import Page, Params from fastapi_pagination.ext.ormar import paginate @@ -6,6 +7,7 @@ from app.depends import check_token from app.models import AuthorAnnotation as AuthorAnnotationDB from app.serializers.author_annotation import AuthorAnnotation + author_annotation_router = APIRouter( prefix="/api/v1/author_annotations", tags=["author_annotation"], diff --git a/fastapi_book_server/app/views/book.py b/fastapi_book_server/app/views/book.py index 0bc75a8..e10ffee 100644 --- a/fastapi_book_server/app/views/book.py +++ b/fastapi_book_server/app/views/book.py @@ -1,6 +1,7 @@ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request, status + from fastapi_pagination import Params from app.depends import check_token, get_allowed_langs @@ -15,7 +16,8 @@ from app.services.book import ( BookMeiliSearchService, GetRandomBookService, ) -from app.utils.pagination import CustomPage +from app.utils.pagination import Page + book_router = APIRouter( prefix="/api/v1/books", @@ -29,9 +31,7 @@ SELECT_RELATED_FIELDS = ["authors", "translators", "annotations"] DETAIL_SELECT_RELATED_FIELDS = ["sequences", "genres"] -@book_router.get( - "/", response_model=CustomPage[RemoteBook], dependencies=[Depends(Params)] -) +@book_router.get("/", response_model=Page[RemoteBook], dependencies=[Depends(Params)]) async def get_books( request: Request, book_filter: dict = Depends(get_book_filter), @@ -40,7 +40,7 @@ async def get_books( @book_router.get( - "/base/", response_model=CustomPage[BookBaseInfo], dependencies=[Depends(Params)] + "/base/", response_model=Page[BookBaseInfo], dependencies=[Depends(Params)] ) async def get_base_books_info( request: Request, book_filter: dict = Depends(get_book_filter) @@ -116,7 +116,7 @@ async def get_book_annotation(id: int): @book_router.get( - "/search/{query}", response_model=CustomPage[Book], dependencies=[Depends(Params)] + "/search/{query}", response_model=Page[Book], dependencies=[Depends(Params)] ) async def search_books( query: str, diff --git a/fastapi_book_server/app/views/book_annotation.py b/fastapi_book_server/app/views/book_annotation.py index 900915e..8ba5d54 100644 --- a/fastapi_book_server/app/views/book_annotation.py +++ b/fastapi_book_server/app/views/book_annotation.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, status + from fastapi_pagination import Page, Params from fastapi_pagination.ext.ormar import paginate @@ -6,6 +7,7 @@ from app.depends import check_token from app.models import BookAnnotation as BookAnnotationDB from app.serializers.book_annotation import BookAnnotation + book_annotation_router = APIRouter( prefix="/api/v1/book_annotations", tags=["book_annotation"], diff --git a/fastapi_book_server/app/views/genre.py b/fastapi_book_server/app/views/genre.py index 8ef2c24..e320aa5 100644 --- a/fastapi_book_server/app/views/genre.py +++ b/fastapi_book_server/app/views/genre.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status + from fastapi_pagination import Params from fastapi_pagination.ext.ormar import paginate @@ -7,7 +8,8 @@ from app.filters.genre import get_genre_filter from app.models import Genre as GenreDB from app.serializers.genre import Genre from app.services.genre import GenreMeiliSearchService -from app.utils.pagination import CustomPage +from app.utils.pagination import Page + genre_router = APIRouter( prefix="/api/v1/genres", tags=["genres"], dependencies=[Depends(check_token)] @@ -17,7 +19,7 @@ genre_router = APIRouter( PREFETCH_RELATED_FIELDS = ["source"] -@genre_router.get("/", response_model=CustomPage[Genre], dependencies=[Depends(Params)]) +@genre_router.get("/", response_model=Page[Genre], dependencies=[Depends(Params)]) async def get_genres(genre_filter: dict = Depends(get_genre_filter)): return await paginate( GenreDB.objects.prefetch_related(PREFETCH_RELATED_FIELDS) @@ -46,7 +48,7 @@ async def get_genre(id: int): @genre_router.get( - "/search/{query}", response_model=CustomPage[Genre], dependencies=[Depends(Params)] + "/search/{query}", response_model=Page[Genre], dependencies=[Depends(Params)] ) async def search_genres( query: str, diff --git a/fastapi_book_server/app/views/healthcheck.py b/fastapi_book_server/app/views/healthcheck.py index bcf338d..ec89004 100644 --- a/fastapi_book_server/app/views/healthcheck.py +++ b/fastapi_book_server/app/views/healthcheck.py @@ -1,5 +1,6 @@ from fastapi import APIRouter + healtcheck_router = APIRouter(tags=["healthcheck"]) diff --git a/fastapi_book_server/app/views/sequence.py b/fastapi_book_server/app/views/sequence.py index c44bbd4..b5553f9 100644 --- a/fastapi_book_server/app/views/sequence.py +++ b/fastapi_book_server/app/views/sequence.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, Request + from fastapi_pagination import Params from fastapi_pagination.ext.ormar import paginate @@ -8,7 +9,8 @@ from app.models import Sequence as SequenceDB from app.serializers.sequence import Book as SequenceBook from app.serializers.sequence import Sequence from app.services.sequence import GetRandomSequenceService, SequenceMeiliSearchService -from app.utils.pagination import CustomPage +from app.utils.pagination import Page + sequence_router = APIRouter( prefix="/api/v1/sequences", @@ -17,9 +19,7 @@ sequence_router = APIRouter( ) -@sequence_router.get( - "/", response_model=CustomPage[Sequence], dependencies=[Depends(Params)] -) +@sequence_router.get("/", response_model=Page[Sequence], dependencies=[Depends(Params)]) async def get_sequences(): return await paginate(SequenceDB.objects) @@ -44,7 +44,7 @@ async def get_sequence(id: int): @sequence_router.get( "/{id}/books", - response_model=CustomPage[SequenceBook], + response_model=Page[SequenceBook], dependencies=[Depends(Params)], ) async def get_sequence_books( @@ -60,7 +60,7 @@ async def get_sequence_books( @sequence_router.get( "/search/{query}", - response_model=CustomPage[Sequence], + response_model=Page[Sequence], dependencies=[Depends(Params)], ) async def search_sequences( diff --git a/fastapi_book_server/app/views/source.py b/fastapi_book_server/app/views/source.py index 34db962..b71a74c 100644 --- a/fastapi_book_server/app/views/source.py +++ b/fastapi_book_server/app/views/source.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends + from fastapi_pagination import Page, Params from fastapi_pagination.ext.ormar import paginate @@ -6,6 +7,7 @@ from app.depends import check_token from app.models import Source as SourceDB from app.serializers.source import Source + source_router = APIRouter( prefix="/api/v1/sources", tags=["source"], diff --git a/fastapi_book_server/app/views/translation.py b/fastapi_book_server/app/views/translation.py index f8ec920..24d7aa3 100644 --- a/fastapi_book_server/app/views/translation.py +++ b/fastapi_book_server/app/views/translation.py @@ -1,11 +1,13 @@ from fastapi import APIRouter, Depends + from fastapi_pagination import Params from fastapi_pagination.ext.ormar import paginate from app.depends import check_token from app.models import Translation as TranslationDB from app.serializers.translation import Translation -from app.utils.pagination import CustomPage +from app.utils.pagination import Page + translation_router = APIRouter( prefix="/api/v1/translation", @@ -15,7 +17,7 @@ translation_router = APIRouter( @translation_router.get( - "/", response_model=CustomPage[Translation], dependencies=[Depends(Params)] + "/", response_model=Page[Translation], dependencies=[Depends(Params)] ) async def get_translations(): return await paginate(TranslationDB.objects.select_related(["book", "author"])) diff --git a/fastapi_book_server/core/app.py b/fastapi_book_server/core/app.py index 255c15c..afbb1bd 100644 --- a/fastapi_book_server/core/app.py +++ b/fastapi_book_server/core/app.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from fastapi.responses import ORJSONResponse + from fastapi_pagination import add_pagination from prometheus_fastapi_instrumentator import Instrumentator from redis import asyncio as aioredis @@ -9,6 +10,7 @@ from app.views import routers from core.config import env_config from core.db import database + sentry_sdk.init( env_config.SENTRY_SDN, ) diff --git a/fastapi_book_server/core/auth.py b/fastapi_book_server/core/auth.py index 61bfbb1..7cc07b5 100644 --- a/fastapi_book_server/core/auth.py +++ b/fastapi_book_server/core/auth.py @@ -1,3 +1,4 @@ from fastapi.security import APIKeyHeader + default_security = APIKeyHeader(name="Authorization") diff --git a/fastapi_book_server/core/db.py b/fastapi_book_server/core/db.py index 0f4b553..0879e69 100644 --- a/fastapi_book_server/core/db.py +++ b/fastapi_book_server/core/db.py @@ -5,6 +5,7 @@ from sqlalchemy import MetaData from core.config import env_config + DATABASE_URL = ( f"postgresql://{env_config.POSTGRES_USER}:{quote(env_config.POSTGRES_PASSWORD)}@" f"{env_config.POSTGRES_HOST}:{env_config.POSTGRES_PORT}/{env_config.POSTGRES_DB}" diff --git a/fastapi_book_server/main.py b/fastapi_book_server/main.py index 2739482..0a4385b 100644 --- a/fastapi_book_server/main.py +++ b/fastapi_book_server/main.py @@ -1,3 +1,4 @@ from core.app import start_app + app = start_app() diff --git a/pyproject.toml b/pyproject.toml index 7983b9f..65f233c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,14 +65,13 @@ max-complexity = 15 [tool.ruff.isort] known-first-party = ["core", "app"] force-sort-within-sections = true +force-wrap-aliases = true +section-order = ["future", "standard-library", "base_framework", "framework_ext", "third-party", "first-party", "local-folder"] +lines-after-imports = 2 -# only_sections = true -# force_sort_within_sections = true -# lines_after_imports = 2 -# lexicographical = true -# sections = ["FUTURE", "STDLIB", "BASEFRAMEWORK", "FRAMEWORKEXT", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] -# known_baseframework = ["fastapi",] -# known_frameworkext = ["starlette",] +[tool.ruff.isort.sections] +base_framework = ["fastapi",] +framework_ext = ["starlette"] [tool.ruff.pyupgrade] keep-runtime-typing = true diff --git a/scripts/healthcheck.py b/scripts/healthcheck.py index 9091896..6f2f501 100644 --- a/scripts/healthcheck.py +++ b/scripts/healthcheck.py @@ -1,5 +1,6 @@ import httpx + response = httpx.get( "http://localhost:8080/healthcheck", )