This commit is contained in:
2023-05-06 00:12:30 +02:00
parent db14333f06
commit 179ac44d7d
24 changed files with 98 additions and 48 deletions

View File

@@ -2,18 +2,17 @@ exclude: 'docs|node_modules|migrations|.git|.tox'
repos: repos:
- repo: https://github.com/ambv/black - repo: https://github.com/ambv/black
rev: 22.12.0 rev: 23.3.0
hooks: hooks:
- id: black - id: black
language_version: python3.11 language_version: python3.11
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.216' rev: 'v0.0.265'
hooks: hooks:
- id: ruff - id: ruff
args: ["--force-exclude"]
- repo: https://github.com/crate-ci/typos - repo: https://github.com/crate-ci/typos
rev: v1.13.6 rev: typos-dict-v0.9.26
hooks: hooks:
- id: typos - id: typos

View File

@@ -3,6 +3,7 @@ from typing import TypedDict
from app.models import Author from app.models import Author
from app.services.common import GetRandomService, MeiliSearchService, TRGMSearchService from app.services.common import GetRandomService, MeiliSearchService, TRGMSearchService
GET_OBJECT_IDS_QUERY = """ GET_OBJECT_IDS_QUERY = """
SELECT ARRAY( SELECT ARRAY(
WITH filtered_authors AS ( WITH filtered_authors AS (

View File

@@ -8,6 +8,7 @@ from app.services.common import (
TRGMSearchService, TRGMSearchService,
) )
GET_OBJECT_IDS_QUERY = """ GET_OBJECT_IDS_QUERY = """
SELECT ARRAY( SELECT ARRAY(
WITH filtered_books AS ( WITH filtered_books AS (

View File

@@ -15,9 +15,10 @@ from redis import asyncio as aioredis
from sqlalchemy import Table from sqlalchemy import Table
from app.utils.orjson_default import default as orjson_default 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 from core.config import env_config
MODEL = TypeVar("MODEL", bound=Model) MODEL = TypeVar("MODEL", bound=Model)
QUERY = TypeVar("QUERY", bound=TypedDict) 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) 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): class SearchQuery(TypedDict):

View File

@@ -3,6 +3,7 @@ from typing import TypedDict
from app.models import Sequence from app.models import Sequence
from app.services.common import GetRandomService, MeiliSearchService, TRGMSearchService from app.services.common import GetRandomService, MeiliSearchService, TRGMSearchService
GET_OBJECT_IDS_QUERY = """ GET_OBJECT_IDS_QUERY = """
SELECT ARRAY ( SELECT ARRAY (
WITH filtered_sequences AS ( WITH filtered_sequences AS (

View File

@@ -1,6 +1,7 @@
from app.models import Author from app.models import Author
from app.services.common import MeiliSearchService, TRGMSearchService from app.services.common import MeiliSearchService, TRGMSearchService
GET_OBJECT_IDS_QUERY = """ GET_OBJECT_IDS_QUERY = """
SELECT ARRAY( SELECT ARRAY(
WITH filtered_authors AS ( WITH filtered_authors AS (

View File

@@ -1,5 +1,7 @@
from typing import Any from typing import Any
import orjson
def default(value: Any): def default(value: Any):
if isinstance(value, frozenset): if isinstance(value, frozenset):
@@ -7,3 +9,7 @@ def default(value: Any):
return "-".join(sorted(list_value)) return "-".join(sorted(list_value))
return value return value
def orjson_dumps(v, *, default) -> str:
return orjson.dumps(v, default=default).decode()

View File

@@ -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 import Params
from fastapi_pagination.bases import AbstractParams from fastapi_pagination.bases import AbstractParams, BasePage
from pydantic import conint from fastapi_pagination.types import GreaterEqualOne, GreaterEqualZero
import orjson
from utils.orjson_default import orjson_dumps
@runtime_checkable @runtime_checkable
@@ -14,23 +24,36 @@ class ToDict(Protocol):
T = TypeVar("T", ToDict, Any) T = TypeVar("T", ToDict, Any)
class CustomPage(Page[T], Generic[T]): class Page(BasePage[T], Generic[T]):
total_pages: conint(ge=0) # type: ignore page: GreaterEqualOne
size: GreaterEqualOne
total_pages: GreaterEqualZero
__params_type__ = Params
class Config:
json_loads = orjson.loads
json_dumps = orjson_dumps
@classmethod @classmethod
def create( def create(
cls, cls,
items: Sequence[T], items: Sequence[T],
total: int,
params: AbstractParams, params: AbstractParams,
) -> Page[T]: *,
total: int,
**kwargs: Any,
) -> "Page[T]":
if not isinstance(params, Params): if not isinstance(params, Params):
raise ValueError("Page should be used with Params") raise ValueError("Page should be used with Params")
pages = ceil(total / params.size)
return cls( return cls(
total=total, total=total,
items=[item.dict() for item in items], items=items,
page=params.page, page=params.page,
size=params.size, size=params.size,
total_pages=(total + params.size - 1) // params.size, total_pages=pages,
**kwargs,
) )

View File

@@ -8,6 +8,7 @@ from app.views.sequence import sequence_router
from app.views.source import source_router from app.views.source import source_router
from app.views.translation import translation_router from app.views.translation import translation_router
routers = [ routers = [
source_router, source_router,
author_router, author_router,

View File

@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi_pagination import Params from fastapi_pagination import Params
from fastapi_pagination.ext.ormar import paginate 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.serializers.author_annotation import AuthorAnnotation
from app.services.author import AuthorMeiliSearchService, GetRandomAuthorService from app.services.author import AuthorMeiliSearchService, GetRandomAuthorService
from app.services.translator import TranslatorMeiliSearchService from app.services.translator import TranslatorMeiliSearchService
from app.utils.pagination import CustomPage from app.utils.pagination import Page
author_router = APIRouter( author_router = APIRouter(
prefix="/api/v1/authors", prefix="/api/v1/authors",
@@ -23,9 +25,7 @@ PREFETCH_RELATED_FIELDS = ["source"]
SELECT_RELATED_FIELDS = ["annotations"] SELECT_RELATED_FIELDS = ["annotations"]
@author_router.get( @author_router.get("/", response_model=Page[Author], dependencies=[Depends(Params)])
"/", response_model=CustomPage[Author], dependencies=[Depends(Params)]
)
async def get_authors(): async def get_authors():
return await paginate( return await paginate(
AuthorDB.objects.select_related(SELECT_RELATED_FIELDS).prefetch_related( AuthorDB.objects.select_related(SELECT_RELATED_FIELDS).prefetch_related(
@@ -75,7 +75,7 @@ async def get_author_annotation(id: int):
@author_router.get( @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( async def get_author_books(
id: int, allowed_langs: list[str] = Depends(get_allowed_langs) id: int, allowed_langs: list[str] = Depends(get_allowed_langs)
@@ -89,7 +89,7 @@ async def get_author_books(
@author_router.get( @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( async def search_authors(
query: str, 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( async def get_translated_books(
id: int, allowed_langs: list[str] = Depends(get_allowed_langs) id: int, allowed_langs: list[str] = Depends(get_allowed_langs)
): ):
@@ -125,7 +125,7 @@ async def get_translated_books(
@translator_router.get( @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( async def search_translators(
query: str, query: str,

View File

@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from fastapi_pagination import Page, Params from fastapi_pagination import Page, Params
from fastapi_pagination.ext.ormar import paginate 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.models import AuthorAnnotation as AuthorAnnotationDB
from app.serializers.author_annotation import AuthorAnnotation from app.serializers.author_annotation import AuthorAnnotation
author_annotation_router = APIRouter( author_annotation_router = APIRouter(
prefix="/api/v1/author_annotations", prefix="/api/v1/author_annotations",
tags=["author_annotation"], tags=["author_annotation"],

View File

@@ -1,6 +1,7 @@
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi_pagination import Params from fastapi_pagination import Params
from app.depends import check_token, get_allowed_langs from app.depends import check_token, get_allowed_langs
@@ -15,7 +16,8 @@ from app.services.book import (
BookMeiliSearchService, BookMeiliSearchService,
GetRandomBookService, GetRandomBookService,
) )
from app.utils.pagination import CustomPage from app.utils.pagination import Page
book_router = APIRouter( book_router = APIRouter(
prefix="/api/v1/books", prefix="/api/v1/books",
@@ -29,9 +31,7 @@ SELECT_RELATED_FIELDS = ["authors", "translators", "annotations"]
DETAIL_SELECT_RELATED_FIELDS = ["sequences", "genres"] DETAIL_SELECT_RELATED_FIELDS = ["sequences", "genres"]
@book_router.get( @book_router.get("/", response_model=Page[RemoteBook], dependencies=[Depends(Params)])
"/", response_model=CustomPage[RemoteBook], dependencies=[Depends(Params)]
)
async def get_books( async def get_books(
request: Request, request: Request,
book_filter: dict = Depends(get_book_filter), book_filter: dict = Depends(get_book_filter),
@@ -40,7 +40,7 @@ async def get_books(
@book_router.get( @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( async def get_base_books_info(
request: Request, book_filter: dict = Depends(get_book_filter) request: Request, book_filter: dict = Depends(get_book_filter)
@@ -116,7 +116,7 @@ async def get_book_annotation(id: int):
@book_router.get( @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( async def search_books(
query: str, query: str,

View File

@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from fastapi_pagination import Page, Params from fastapi_pagination import Page, Params
from fastapi_pagination.ext.ormar import paginate 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.models import BookAnnotation as BookAnnotationDB
from app.serializers.book_annotation import BookAnnotation from app.serializers.book_annotation import BookAnnotation
book_annotation_router = APIRouter( book_annotation_router = APIRouter(
prefix="/api/v1/book_annotations", prefix="/api/v1/book_annotations",
tags=["book_annotation"], tags=["book_annotation"],

View File

@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi_pagination import Params from fastapi_pagination import Params
from fastapi_pagination.ext.ormar import paginate 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.models import Genre as GenreDB
from app.serializers.genre import Genre from app.serializers.genre import Genre
from app.services.genre import GenreMeiliSearchService from app.services.genre import GenreMeiliSearchService
from app.utils.pagination import CustomPage from app.utils.pagination import Page
genre_router = APIRouter( genre_router = APIRouter(
prefix="/api/v1/genres", tags=["genres"], dependencies=[Depends(check_token)] prefix="/api/v1/genres", tags=["genres"], dependencies=[Depends(check_token)]
@@ -17,7 +19,7 @@ genre_router = APIRouter(
PREFETCH_RELATED_FIELDS = ["source"] 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)): async def get_genres(genre_filter: dict = Depends(get_genre_filter)):
return await paginate( return await paginate(
GenreDB.objects.prefetch_related(PREFETCH_RELATED_FIELDS) GenreDB.objects.prefetch_related(PREFETCH_RELATED_FIELDS)
@@ -46,7 +48,7 @@ async def get_genre(id: int):
@genre_router.get( @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( async def search_genres(
query: str, query: str,

View File

@@ -1,5 +1,6 @@
from fastapi import APIRouter from fastapi import APIRouter
healtcheck_router = APIRouter(tags=["healthcheck"]) healtcheck_router = APIRouter(tags=["healthcheck"])

View File

@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from fastapi_pagination import Params from fastapi_pagination import Params
from fastapi_pagination.ext.ormar import paginate 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 Book as SequenceBook
from app.serializers.sequence import Sequence from app.serializers.sequence import Sequence
from app.services.sequence import GetRandomSequenceService, SequenceMeiliSearchService from app.services.sequence import GetRandomSequenceService, SequenceMeiliSearchService
from app.utils.pagination import CustomPage from app.utils.pagination import Page
sequence_router = APIRouter( sequence_router = APIRouter(
prefix="/api/v1/sequences", prefix="/api/v1/sequences",
@@ -17,9 +19,7 @@ sequence_router = APIRouter(
) )
@sequence_router.get( @sequence_router.get("/", response_model=Page[Sequence], dependencies=[Depends(Params)])
"/", response_model=CustomPage[Sequence], dependencies=[Depends(Params)]
)
async def get_sequences(): async def get_sequences():
return await paginate(SequenceDB.objects) return await paginate(SequenceDB.objects)
@@ -44,7 +44,7 @@ async def get_sequence(id: int):
@sequence_router.get( @sequence_router.get(
"/{id}/books", "/{id}/books",
response_model=CustomPage[SequenceBook], response_model=Page[SequenceBook],
dependencies=[Depends(Params)], dependencies=[Depends(Params)],
) )
async def get_sequence_books( async def get_sequence_books(
@@ -60,7 +60,7 @@ async def get_sequence_books(
@sequence_router.get( @sequence_router.get(
"/search/{query}", "/search/{query}",
response_model=CustomPage[Sequence], response_model=Page[Sequence],
dependencies=[Depends(Params)], dependencies=[Depends(Params)],
) )
async def search_sequences( async def search_sequences(

View File

@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from fastapi_pagination import Page, Params from fastapi_pagination import Page, Params
from fastapi_pagination.ext.ormar import paginate 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.models import Source as SourceDB
from app.serializers.source import Source from app.serializers.source import Source
source_router = APIRouter( source_router = APIRouter(
prefix="/api/v1/sources", prefix="/api/v1/sources",
tags=["source"], tags=["source"],

View File

@@ -1,11 +1,13 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from fastapi_pagination import Params from fastapi_pagination import Params
from fastapi_pagination.ext.ormar import paginate from fastapi_pagination.ext.ormar import paginate
from app.depends import check_token from app.depends import check_token
from app.models import Translation as TranslationDB from app.models import Translation as TranslationDB
from app.serializers.translation import Translation from app.serializers.translation import Translation
from app.utils.pagination import CustomPage from app.utils.pagination import Page
translation_router = APIRouter( translation_router = APIRouter(
prefix="/api/v1/translation", prefix="/api/v1/translation",
@@ -15,7 +17,7 @@ translation_router = APIRouter(
@translation_router.get( @translation_router.get(
"/", response_model=CustomPage[Translation], dependencies=[Depends(Params)] "/", response_model=Page[Translation], dependencies=[Depends(Params)]
) )
async def get_translations(): async def get_translations():
return await paginate(TranslationDB.objects.select_related(["book", "author"])) return await paginate(TranslationDB.objects.select_related(["book", "author"]))

View File

@@ -1,5 +1,6 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from fastapi_pagination import add_pagination from fastapi_pagination import add_pagination
from prometheus_fastapi_instrumentator import Instrumentator from prometheus_fastapi_instrumentator import Instrumentator
from redis import asyncio as aioredis from redis import asyncio as aioredis
@@ -9,6 +10,7 @@ from app.views import routers
from core.config import env_config from core.config import env_config
from core.db import database from core.db import database
sentry_sdk.init( sentry_sdk.init(
env_config.SENTRY_SDN, env_config.SENTRY_SDN,
) )

View File

@@ -1,3 +1,4 @@
from fastapi.security import APIKeyHeader from fastapi.security import APIKeyHeader
default_security = APIKeyHeader(name="Authorization") default_security = APIKeyHeader(name="Authorization")

View File

@@ -5,6 +5,7 @@ from sqlalchemy import MetaData
from core.config import env_config from core.config import env_config
DATABASE_URL = ( DATABASE_URL = (
f"postgresql://{env_config.POSTGRES_USER}:{quote(env_config.POSTGRES_PASSWORD)}@" f"postgresql://{env_config.POSTGRES_USER}:{quote(env_config.POSTGRES_PASSWORD)}@"
f"{env_config.POSTGRES_HOST}:{env_config.POSTGRES_PORT}/{env_config.POSTGRES_DB}" f"{env_config.POSTGRES_HOST}:{env_config.POSTGRES_PORT}/{env_config.POSTGRES_DB}"

View File

@@ -1,3 +1,4 @@
from core.app import start_app from core.app import start_app
app = start_app() app = start_app()

View File

@@ -65,14 +65,13 @@ max-complexity = 15
[tool.ruff.isort] [tool.ruff.isort]
known-first-party = ["core", "app"] known-first-party = ["core", "app"]
force-sort-within-sections = true 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 [tool.ruff.isort.sections]
# force_sort_within_sections = true base_framework = ["fastapi",]
# lines_after_imports = 2 framework_ext = ["starlette"]
# lexicographical = true
# sections = ["FUTURE", "STDLIB", "BASEFRAMEWORK", "FRAMEWORKEXT", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
# known_baseframework = ["fastapi",]
# known_frameworkext = ["starlette",]
[tool.ruff.pyupgrade] [tool.ruff.pyupgrade]
keep-runtime-typing = true keep-runtime-typing = true

View File

@@ -1,5 +1,6 @@
import httpx import httpx
response = httpx.get( response = httpx.get(
"http://localhost:8080/healthcheck", "http://localhost:8080/healthcheck",
) )