Skip to content

Commit

Permalink
DRF & ninja support (#4)
Browse files Browse the repository at this point in the history
* add drf tests

* lint

* better tests + refactoring

* lint

* more test fixes

* ninja tests

* lint
  • Loading branch information
andriykohut authored Apr 16, 2024
1 parent 30a2975 commit 7c119ce
Show file tree
Hide file tree
Showing 14 changed files with 663 additions and 189 deletions.
22 changes: 22 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)


if __name__ == "__main__":
main()
181 changes: 177 additions & 4 deletions poetry.lock

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,18 @@ pymemcache = "^4.0.0"
redis = "^5.0.3"
pytest-cov = "^5.0.0"
mkdocs = "^1.5.3"
mkdocs-material = "^9.5.17"
mkdocs-material = "^9.5.18"
djangorestframework = "^3.15.1"
django-ninja = "^1.1.0"
pytest-django = "^4.8.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "test_app.settings"

[[tool.mypy.overrides]]
module = "rest_framework.*"
ignore_missing_imports = true
Empty file added test_app/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions test_app/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
ASGI config for test_app project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")

application = get_asgi_application()
151 changes: 151 additions & 0 deletions test_app/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""
Django settings for test_app project.
Generated by 'django-admin startproject' using Django 4.2.11.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-+wwvf-7vu5_ptav^qkyn8z1gt*s%n)%u34bglb481#^awz^j(3"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ["localhost", "127.0.0.1"]


# Application definition

INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
]

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "test_app.urls"

TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]

WSGI_APPLICATION = "test_app.wsgi.application"


# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
}


# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]


# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = "static/"

# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
},
"locmem": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
},
"memcached": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "127.0.0.1:11211",
},
"filebased": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/tmp/django_cache",
},
"redis": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
},
"db": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "cache",
},
}
47 changes: 47 additions & 0 deletions test_app/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
URL configuration for test_app project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""

from django.contrib import admin
from django.urls import path
from rest_framework.routers import DefaultRouter

from test_app import views

router = DefaultRouter()
router.register(r"drf/viewset", views.TestViewSet, basename="viewset")

urlpatterns = [
path("admin/", admin.site.urls),
path("defaults/<int:count>/", views.defaults, name="defaults"),
path("by-string-key/", views.by_string_key, name="by_string_key"),
path("by-func-key/", views.by_func_key, name="by_func_key"),
path("by-method/", views.by_method, name="by_method"),
path(
"fixed-window-elastic-expiry/",
views.fixed_window_elastic_expiry,
name="fixed_window_elastic_expiry",
),
path("teapot/", views.teapot, name="teapot"),
path("cbv/", views.TestView.as_view()),
path("storage/redis/", views.redis, name="redis_storage"),
path("storage/memory/", views.memory, name="memory_storage"),
path("storage/memcached/", views.memcached, name="memcached_storage"),
path("drf/api-view/", views.drf_api_view, name="drf_api_view"),
path("drf/view/", views.TestDRFView.as_view(), name="drf_view"),
path("ninja/", views.api.urls),
*router.urls,
]
104 changes: 104 additions & 0 deletions test_app/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from django.contrib.auth.models import User
from django.http import HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_http_methods
from limits.storage import MemoryStorage, RedisStorage, MemcachedStorage
from ninja import NinjaAPI
from rest_framework import viewsets, serializers, views
from rest_framework.decorators import api_view
from rest_framework.request import Request
from rest_framework.response import Response

from django_ratelimiter import ratelimit


@ratelimit("5/minute")
def defaults(_: HttpRequest, count: int) -> HttpResponse:
return HttpResponse(f"{count}")


@ratelimit("5/minute", key="user")
def by_string_key(request: HttpRequest) -> HttpResponse:
return HttpResponse(request.user.pk)


@ratelimit("5/minute", key=lambda r: r.user.get_username())
def by_func_key(request: HttpRequest) -> HttpResponse:
return HttpResponse(request.user.pk)


@require_http_methods(["POST", "PUT", "GET"])
@ratelimit("5/minute", methods=["POST", "PUT"])
def by_method(_: HttpRequest) -> HttpResponse:
return HttpResponse("OK")


@ratelimit("5/minute", strategy="fixed-window-elastic-expiry")
def fixed_window_elastic_expiry(_: HttpRequest) -> HttpResponse:
return HttpResponse("OK")


@ratelimit("1/minute", response=HttpResponse(status=418))
def teapot(_: HttpRequest) -> HttpResponse:
return HttpResponse("OK")


@method_decorator(ratelimit("1/minute"), name="dispatch")
class TestView(View):
def get(self, _: HttpRequest) -> HttpResponse:
return HttpResponse("OK")

def post(self, _: HttpRequest) -> HttpResponse:
return HttpResponse("OK")


memory_storage = MemoryStorage()
redis_storage = RedisStorage(uri="redis://localhost:6379/0")
memcached_storage = MemcachedStorage(uri="memcached://localhost:11211")


@ratelimit("5/minute", storage=memory_storage)
def memory(_: HttpRequest) -> HttpResponse:
return HttpResponse("OK")


@ratelimit("5/minute", storage=redis_storage)
def redis(_: HttpRequest) -> HttpResponse:
return HttpResponse("OK")


@ratelimit("5/minute", storage=memcached_storage)
def memcached(_: HttpRequest) -> HttpResponse:
return HttpResponse("OK")


@api_view(["GET"])
@ratelimit("5/minute")
def drf_api_view(_: Request) -> Response:
return Response({"hello": "world"})


class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User


@method_decorator(ratelimit("5/minute"), name="dispatch")
class TestViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer


@method_decorator(ratelimit("5/minute"), name="dispatch")
class TestDRFView(views.APIView):
def get(self, _: Request) -> Response:
return Response("200")


api = NinjaAPI()


@api.get("/add")
def add(request: HttpRequest, a: int, b: int):
return {"result": a + b}
16 changes: 16 additions & 0 deletions test_app/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
WSGI config for test_app project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")

application = get_wsgi_application()
Empty file added tests/__init__.py
Empty file.
Loading

0 comments on commit 7c119ce

Please sign in to comment.