Skip to content

Commit

Permalink
Merge pull request #37 from stephanelatil/main
Browse files Browse the repository at this point in the history
Pull Request for #26
  • Loading branch information
em1208 authored Aug 3, 2024
2 parents 324e633 + 462ceed commit 29af16a
Show file tree
Hide file tree
Showing 28 changed files with 740 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install poetry
run: python -m pip install poetry==1.8.2
run: python -m pip install poetry==1.8.3

- name: Install dependencies
run: |
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v4.6.0
hooks:
- id: check-ast
- id: check-added-large-files
Expand All @@ -12,7 +12,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.3
rev: v0.5.5
hooks:
# Run the linter.
- id: ruff
Expand Down
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class AsyncSerializer(Serializer):
views.py

```python
from . import serializers
from .serializers import AsyncSerializer
from adrf.views import APIView

class AsyncView(APIView):
Expand All @@ -149,7 +149,42 @@ class AsyncView(APIView):
"password": "test",
"age": 10,
}
serializer = serializers.AsyncSerializer(data=data)
serializer = AsyncSerializer(data=data)
serializer.is_valid()
return await serializer.adata
```

# Async generics

models.py

```python
from django.db import models

class Order(models.Model):
name = models.TextField()
```

serializers.py

```python
from adrf.serializers import ModelSerializer
from .models import Order

class OrderSerializer(ModelSerializer):
class Meta:
model = Order
fields = ('name', )
```

views.py

```python
from adrf.generics import ListCreateAPIView
from .models import Order
from .serializers import OrderSerializer

class ListCreateOrderView(ListCreateAPIView):
queryset = Order.objects.all()
serializer_class = OrderSerializer
```
212 changes: 212 additions & 0 deletions adrf/generics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import asyncio

from asgiref.sync import async_to_sync
from django.http import Http404
from rest_framework.exceptions import ValidationError
from rest_framework.generics import GenericAPIView as DRFGenericAPIView

from adrf import mixins, views
from adrf.shortcuts import aget_object_or_404 as _aget_object_or_404


def aget_object_or_404(queryset, *filter_args, **filter_kwargs):
"""
Same as Django's standard shortcut, but make sure to also raise 404
if the filter_kwargs don't match the required types.
"""
try:
return _aget_object_or_404(queryset, *filter_args, **filter_kwargs)
except (TypeError, ValueError, ValidationError):
raise Http404


class GenericAPIView(views.APIView, DRFGenericAPIView):
"""This generic API view supports async pagination."""

async def aget_object(self):
"""
Returns the object the view is displaying.
You may want to override this if you need to provide non-standard
queryset lookups. Eg if objects are referenced using multiple
keyword arguments in the url conf.
"""
queryset = self.filter_queryset(self.get_queryset())

# Perform the lookup filtering.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

assert lookup_url_kwarg in self.kwargs, (
"Expected view %s to be called with a URL keyword argument "
'named "%s". Fix your URL conf, or set the `.lookup_field` '
"attribute on the view correctly."
% (self.__class__.__name__, lookup_url_kwarg)
)

filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = await aget_object_or_404(queryset, **filter_kwargs)

# May raise a permission denied
self.check_object_permissions(self.request, obj)

return obj

def paginate_queryset(self, queryset):
"""
Return a single page of results, or `None` if pagination is disabled.
"""
if self.paginator is None:
return None
if asyncio.iscoroutinefunction(self.paginator.paginate_queryset):
return async_to_sync(self.paginator.paginate_queryset)(
queryset, self.request, view=self
)
return self.paginator.paginate_queryset(queryset, self.request, view=self)

def get_paginated_response(self, data):
"""
Return a paginated style `Response` object for the given output data.
"""
assert self.paginator is not None
if asyncio.iscoroutinefunction(self.paginator.get_paginated_response):
return async_to_sync(self.paginator.get_paginated_response)(data)
return self.paginator.get_paginated_response(data)

async def apaginate_queryset(self, queryset):
"""
Return a single page of results, or `None` if pagination is disabled.
"""
if self.paginator is None:
return None
if asyncio.iscoroutinefunction(self.paginator.paginate_queryset):
return await self.paginator.paginate_queryset(
queryset, self.request, view=self
)
return self.paginator.paginate_queryset(queryset, self.request, view=self)

async def get_apaginated_response(self, data):
"""
Return a paginated style `Response` object for the given output data.
"""
assert self.paginator is not None
if asyncio.iscoroutinefunction(self.paginator.get_paginated_response):
return await self.paginator.get_paginated_response(data)
return self.paginator.get_paginated_response(data)


# Concrete view classes that provide method handlers
# by composing the mixin classes with the base view.


class CreateAPIView(mixins.CreateModelMixin, GenericAPIView):
"""
Concrete view for creating a model instance.
"""

async def post(self, request, *args, **kwargs):
return await self.acreate(request, *args, **kwargs)


class ListAPIView(mixins.ListModelMixin, GenericAPIView):
"""
Concrete view for listing a queryset.
"""

async def get(self, request, *args, **kwargs):
return await self.alist(request, *args, **kwargs)


class RetrieveAPIView(mixins.RetrieveModelMixin, GenericAPIView):
"""
Concrete view for retrieving a model instance.
"""

async def get(self, request, *args, **kwargs):
return await self.aretrieve(request, *args, **kwargs)


class DestroyAPIView(mixins.DestroyModelMixin, GenericAPIView):
"""
Concrete view for deleting a model instance.
"""

async def delete(self, request, *args, **kwargs):
return await self.adestroy(request, *args, **kwargs)


class UpdateAPIView(mixins.UpdateModelMixin, GenericAPIView):
"""
Concrete view for updating a model instance.
"""

async def put(self, request, *args, **kwargs):
return await self.aupdate(request, *args, **kwargs)

async def patch(self, request, *args, **kwargs):
return await self.partial_aupdate(request, *args, **kwargs)


class ListCreateAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, GenericAPIView):
"""
Concrete view for listing a queryset or creating a model instance.
"""

async def get(self, request, *args, **kwargs):
return await self.alist(request, *args, **kwargs)

async def post(self, request, *args, **kwargs):
return await self.acreate(request, *args, **kwargs)


class RetrieveUpdateAPIView(
mixins.RetrieveModelMixin, mixins.UpdateModelMixin, GenericAPIView
):
"""
Concrete view for retrieving, updating a model instance.
"""

async def get(self, request, *args, **kwargs):
return await self.aretrieve(request, *args, **kwargs)

async def put(self, request, *args, **kwargs):
return await self.aupdate(request, *args, **kwargs)

async def patch(self, request, *args, **kwargs):
return await self.partial_aupdate(request, *args, **kwargs)


class RetrieveDestroyAPIView(
mixins.RetrieveModelMixin, mixins.DestroyModelMixin, GenericAPIView
):
"""
Concrete view for retrieving or deleting a model instance.
"""

async def get(self, request, *args, **kwargs):
return await self.aretrieve(request, *args, **kwargs)

async def delete(self, request, *args, **kwargs):
return await self.adestroy(request, *args, **kwargs)


class RetrieveUpdateDestroyAPIView(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
GenericAPIView,
):
"""
Concrete view for retrieving, updating or deleting a model instance.
"""

async def get(self, request, *args, **kwargs):
return await self.aretrieve(request, *args, **kwargs)

async def put(self, request, *args, **kwargs):
return await self.aupdate(request, *args, **kwargs)

async def patch(self, request, *args, **kwargs):
return await self.partial_aupdate(request, *args, **kwargs)

async def delete(self, request, *args, **kwargs):
return await self.adestroy(request, *args, **kwargs)
98 changes: 98 additions & 0 deletions adrf/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from asgiref.sync import sync_to_async
from rest_framework import mixins, status
from rest_framework.response import Response


async def get_data(serializer):
"""Use adata if the serializer supports it, data otherwise."""
return await serializer.adata if hasattr(serializer, "adata") else serializer.data


class CreateModelMixin(mixins.CreateModelMixin):
"""
Create a model instance.
"""

async def acreate(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
await sync_to_async(serializer.is_valid)(raise_exception=True)
await self.perform_acreate(serializer)
data = await get_data(serializer)
headers = self.get_success_headers(data)
return Response(data, status=status.HTTP_201_CREATED, headers=headers)

async def perform_acreate(self, serializer):
await serializer.asave()


class ListModelMixin(mixins.ListModelMixin):
"""
List a queryset.
"""

async def alist(self, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())

page = await self.apaginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
data = await get_data(serializer)
return await self.get_apaginated_response(data)

serializer = self.get_serializer(queryset, many=True)
data = await get_data(serializer)
return Response(data, status=status.HTTP_200_OK)


class RetrieveModelMixin(mixins.RetrieveModelMixin):
"""
Retrieve a model instance.
"""

async def aretrieve(self, request, *args, **kwargs):
instance = await self.aget_object()
serializer = self.get_serializer(instance, many=False)
data = await get_data(serializer)
return Response(data, status=status.HTTP_200_OK)


class UpdateModelMixin(mixins.UpdateModelMixin):
"""
Update a model instance.
"""

async def aupdate(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
instance = await self.aget_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
await sync_to_async(serializer.is_valid)(raise_exception=True)
await self.perform_aupdate(serializer)

if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
data = await get_data(serializer)

return Response(data, status=status.HTTP_200_OK)

async def perform_aupdate(self, serializer):
await serializer.asave()

async def partial_aupdate(self, request, *args, **kwargs):
kwargs["partial"] = True
return await self.aupdate(request, *args, **kwargs)


class DestroyModelMixin(mixins.DestroyModelMixin):
"""
Destroy a model instance.
"""

async def adestroy(self, request, *args, **kwargs):
instance = await self.aget_object()
await self.perform_adestroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)

async def perform_adestroy(self, instance):
await instance.adelete()
1 change: 0 additions & 1 deletion adrf/requests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import asyncio

from asgiref.sync import async_to_sync

from rest_framework import exceptions
from rest_framework.request import Request, wrap_attributeerrors

Expand Down
Loading

0 comments on commit 29af16a

Please sign in to comment.