Skip to content

Commit

Permalink
feat(api): make API async
Browse files Browse the repository at this point in the history
See #318
  • Loading branch information
Jenselme committed Nov 26, 2024
1 parent d2b8cfe commit 7fcb210
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 19 deletions.
1 change: 1 addition & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ def before_send_to_sentry(event, hint):
# ------------------------------------------------------------------------------
# See https://django-ninja.dev/reference/settings/
NINJA_PAGINATION_MAX_LIMIT = 500
NINJA_PAGINATION_CLASS = "legadilo.utils.pagination.LimitOffsetPagination"


# Your stuff...
Expand Down
12 changes: 6 additions & 6 deletions legadilo/reading/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from operator import xor
from typing import Annotated, Self

from asgiref.sync import async_to_sync
from asgiref.sync import sync_to_async
from ninja import ModelSchema, Router, Schema
from ninja.pagination import paginate
from pydantic import Field, model_validator
Expand Down Expand Up @@ -59,25 +59,25 @@ def has_data(self) -> bool:

@reading_api_router.get("/articles/", response=list[OutArticleSchema])
@paginate
def list_articles(request: AuthenticateApiRequest):
async def list_articles(request: AuthenticateApiRequest): # noqa: RUF029 pagination is async!
return Article.objects.get_queryset().for_user(request.auth).default_order_by()


@reading_api_router.post("/articles/", response=OutArticleSchema)
def create_article(request: AuthenticateApiRequest, article: InArticleSchema):
async def create_article(request: AuthenticateApiRequest, article: InArticleSchema):
if article.has_data:
article_data = build_article_data_from_content(
url=article.link, title=article.title, content=article.content
)
else:
article_data = async_to_sync(get_article_from_url)(article.link)
article_data = await get_article_from_url(article.link)

# Tags specified in article data are the raw tags used in feeds, they are not used to link an
# article to tag objects.
tags = Tag.objects.get_or_create_from_list(request.auth, article.tags)
tags = await sync_to_async(Tag.objects.get_or_create_from_list)(request.auth, article.tags)
article_data = article_data.model_copy(update={"tags": ()})

articles = Article.objects.update_or_create_from_articles_list(
articles = await sync_to_async(Article.objects.update_or_create_from_articles_list)(
request.auth, [article_data], tags, source_type=constants.ArticleSourceType.MANUAL
)
return articles[0]
16 changes: 8 additions & 8 deletions legadilo/users/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import jwt
from django.core.exceptions import BadRequest
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.shortcuts import aget_object_or_404
from ninja import Router, Schema
from ninja.security import HttpBearer
from pydantic import BaseModel as BaseSchema
Expand All @@ -35,12 +35,12 @@


class AuthBearer(HttpBearer):
def authenticate(self, request, token) -> User | None:
async def authenticate(self, request, token) -> User | None:
if not token:
return None

decoded_jwt = _decode_jwt(token)
return _get_user_from_jwt(decoded_jwt)
return await _get_user_from_jwt(decoded_jwt)


class JWT(BaseSchema):
Expand All @@ -59,9 +59,9 @@ def _decode_jwt(token: str) -> JWT:
raise BadRequest("Invalid JWT token") from e


def _get_user_from_jwt(decoded_jwt: JWT) -> User | None:
async def _get_user_from_jwt(decoded_jwt: JWT) -> User | None:
try:
return User.objects.get(id=decoded_jwt.user_id)
return await User.objects.aget(id=decoded_jwt.user_id)
except User.DoesNotExist:
return None

Expand All @@ -75,13 +75,13 @@ class Token(Schema):


@users_api_router.post("/refresh/", auth=None, response=Token)
def refresh_token(request: HttpRequest, payload: RefreshTokenPayload) -> Token:
application_token = get_object_or_404(
async def refresh_token(request: HttpRequest, payload: RefreshTokenPayload) -> Token:
application_token = await aget_object_or_404(
ApplicationToken.objects.get_queryset().only_valid().defer(None),
token=payload.application_token,
)
application_token.last_used_at = utcnow()
application_token.save()
await application_token.asave()
jwt = _create_jwt(application_token.user_id, application_token.token)

return Token(jwt=jwt)
Expand Down
6 changes: 1 addition & 5 deletions legadilo/utils/collections_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,7 @@ def max_or_none(


async def alist(collection: AsyncIterable[T]) -> list[T]:
output = []
async for item in collection:
output.append(item)

return output
return [item async for item in collection]


async def aset(collection: AsyncIterable[T]) -> set[T]:
Expand Down
24 changes: 24 additions & 0 deletions legadilo/utils/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Any

from django.core.paginator import Page, Paginator
from django.db.models import QuerySet
from ninja.pagination import LimitOffsetPagination as NinjaLimitOffsetPagination

from legadilo.utils.collections_utils import alist


def get_requested_page(paginator: Paginator, requested_page: int) -> Page:
Expand All @@ -23,3 +28,22 @@ def get_requested_page(paginator: Paginator, requested_page: int) -> Page:
if 1 <= requested_page <= paginator.num_pages
else paginator.page(1)
)


class LimitOffsetPagination(NinjaLimitOffsetPagination):
"""Custom paginator to fix a bug in Ninja pagination.
There is a bug in Ninja when we try to paginate querysets in async context: we will get a
SynchronousOnlyOperation error. This should be solved "soon" with
https://github.com/vitalik/django-ninja/pull/1340
"""

async def apaginate_queryset(
self,
queryset: QuerySet,
pagination: Any,
**params: Any,
) -> Any:
result = await super().apaginate_queryset(queryset, pagination, **params)
result["items"] = await alist(result["items"])
return result

0 comments on commit 7fcb210

Please sign in to comment.