Skip to content

Commit

Permalink
Reimplement Django middleware (#17)
Browse files Browse the repository at this point in the history
* Reimplement Django middleware

* Fix

* Add uritemplate dependency

* Add inflection dependency

* Add catch-all exception handling

* Support multiple urlconfs

* Fix

* Improve response time measurement

* Add no cover comments

* Remove django extra
  • Loading branch information
itssimon authored Mar 22, 2024
1 parent d8542d7 commit 1fa4cd2
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 211 deletions.
9 changes: 4 additions & 5 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,16 @@ jobs:
- flask
- flask==2.3.*
- flask==2.0.3 Werkzeug==2.*
- djangorestframework django
- djangorestframework django==4.2.*
- djangorestframework==3.12.* django==3.2.*
- djangorestframework==3.10.* django==2.2.*
- djangorestframework django uritemplate inflection
- djangorestframework django==4.2.* uritemplate inflection
- djangorestframework==3.12.* django==3.2.* uritemplate
- djangorestframework==3.10.* django==2.2.* uritemplate
- django-ninja django
- django-ninja==0.22.* django
- django-ninja==0.18.0 django
- litestar
- litestar==2.6.1
- litestar==2.0.1

steps:
- uses: actions/checkout@v4
- name: Load cached Poetry installation
Expand Down
354 changes: 193 additions & 161 deletions apitally/django.py

Large diffs are not rendered by default.

54 changes: 32 additions & 22 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ djangorestframework = { version = ">=3.10.0", optional = true }
fastapi = { version = ">=0.87.0", optional = true }
flask = { version = ">=2.0.0", optional = true }
httpx = { version = ">=0.22.0", optional = true }
inflection = { version = ">=0.5.1", optional = true }
litestar = { version = ">=2.0.0", optional = true }
requests = { version = ">=2.26.0", optional = true }
starlette = { version = ">=0.21.0,<1.0.0", optional = true }
uritemplate = { version = ">=3.0.0", optional = true }

[tool.poetry.group.dev.dependencies]
ipykernel = "^6.26.0"
Expand Down Expand Up @@ -65,9 +67,14 @@ types-six = "*"
types-ujson = "*"

[tool.poetry.extras]
django = ["django", "requests"]
django_ninja = ["django", "django-ninja", "requests"]
django_rest_framework = ["django", "djangorestframework", "requests"]
django_rest_framework = [
"django",
"djangorestframework",
"uritemplate", # required for schema generation
"inflection", # required for schema generation
"requests",
]
fastapi = ["fastapi", "starlette", "httpx"]
flask = ["flask", "requests"]
litestar = ["litestar", "httpx"]
Expand Down
2 changes: 1 addition & 1 deletion tests/django_ninja_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
api = NinjaAPI()


@api.get("/foo")
@api.get("/foo", summary="Foo", description="Foo")
def foo(request: HttpRequest) -> str:
return "foo"

Expand Down
2 changes: 2 additions & 0 deletions tests/django_rest_framework_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


class FooView(APIView):
"""Foo"""

def get(self, request: Request) -> Response:
return Response("foo")

Expand Down
59 changes: 46 additions & 13 deletions tests/test_django_ninja.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def test_middleware_requests_ok(client: Client, mocker: MockerFixture):
mock.assert_called_once()
assert mock.call_args is not None
assert mock.call_args.kwargs["method"] == "GET"
assert mock.call_args.kwargs["path"] == "api/foo/<bar>"
assert mock.call_args.kwargs["path"] == "/api/foo/{bar}"
assert mock.call_args.kwargs["status_code"] == 200
assert mock.call_args.kwargs["response_time"] > 0
assert int(mock.call_args.kwargs["response_size"]) > 0
Expand All @@ -88,6 +88,14 @@ def test_middleware_requests_ok(client: Client, mocker: MockerFixture):
assert int(mock.call_args.kwargs["request_size"]) > 0


def test_middleware_requests_404(client: Client, mocker: MockerFixture):
mock = mocker.patch("apitally.client.base.RequestCounter.add_request")

response = client.get("/api/none")
assert response.status_code == 404
mock.assert_not_called()


def test_middleware_requests_error(client: Client, mocker: MockerFixture):
mock = mocker.patch("apitally.client.base.RequestCounter.add_request")

Expand All @@ -96,7 +104,7 @@ def test_middleware_requests_error(client: Client, mocker: MockerFixture):
mock.assert_called_once()
assert mock.call_args is not None
assert mock.call_args.kwargs["method"] == "PUT"
assert mock.call_args.kwargs["path"] == "api/baz"
assert mock.call_args.kwargs["path"] == "/api/baz"
assert mock.call_args.kwargs["status_code"] == 500
assert mock.call_args.kwargs["response_time"] > 0

Expand All @@ -109,22 +117,47 @@ def test_middleware_validation_error(client: Client, mocker: MockerFixture):
mock.assert_called_once()
assert mock.call_args is not None
assert mock.call_args.kwargs["method"] == "GET"
assert mock.call_args.kwargs["path"] == "api/val"
assert mock.call_args.kwargs["path"] == "/api/val"
assert len(mock.call_args.kwargs["detail"]) == 1
assert mock.call_args.kwargs["detail"][0]["loc"] == ["query", "foo"]


def test_get_app_info(mocker: MockerFixture):
from django.urls import get_resolver
def test_get_app_info():
from apitally.django import _get_app_info

from apitally.django import _extract_views_from_url_patterns, _get_app_info

views = _extract_views_from_url_patterns(get_resolver().url_patterns)

app_info = _get_app_info(views=views)
app_info = _get_app_info(app_version="1.2.3", urlconfs=[None])
openapi = json.loads(app_info["openapi"])
assert len(app_info["paths"]) == len(openapi["paths"])
assert len(app_info["paths"]) == 5
assert len(openapi["paths"]) == 5

app_info = _get_app_info(views=views, app_version="1.2.3", openapi_url="/api/openapi.json")
assert "openapi" in app_info
assert app_info["versions"]["django"]
assert app_info["versions"]["django-ninja"]
assert app_info["versions"]["app"] == "1.2.3"
assert app_info["client"] == "python:django"


def test_get_ninja_api_instances():
from ninja import NinjaAPI

from apitally.django import _get_ninja_api_instances

apis = _get_ninja_api_instances()
assert len(apis) == 1
api = list(apis)[0]
assert isinstance(api, NinjaAPI)


def test_get_ninja_api_endpoints():
from apitally.django import _get_ninja_paths

endpoints = _get_ninja_paths([None])
assert len(endpoints) == 5
assert all(len(e["summary"]) > 0 for e in endpoints)
assert any(e["description"] is not None and len(e["description"]) > 0 for e in endpoints)


def test_check_import():
from apitally.django import _check_import

assert _check_import("ninja") is True
assert _check_import("nonexistentpackage") is False
34 changes: 27 additions & 7 deletions tests/test_django_rest_framework.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import json
import sys
from importlib.util import find_spec
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -71,7 +72,7 @@ def test_middleware_requests_ok(client: APIClient, mocker: MockerFixture):
mock.assert_called_once()
assert mock.call_args is not None
assert mock.call_args.kwargs["method"] == "GET"
assert mock.call_args.kwargs["path"] == "foo/<int:bar>/"
assert mock.call_args.kwargs["path"] == "/foo/{bar}/"
assert mock.call_args.kwargs["status_code"] == 200
assert mock.call_args.kwargs["response_time"] > 0
assert int(mock.call_args.kwargs["response_size"]) > 0
Expand All @@ -84,6 +85,14 @@ def test_middleware_requests_ok(client: APIClient, mocker: MockerFixture):
assert int(mock.call_args.kwargs["request_size"]) > 0


def test_middleware_requests_404(client: APIClient, mocker: MockerFixture):
mock = mocker.patch("apitally.client.base.RequestCounter.add_request")

response = client.get("/api/none")
assert response.status_code == 404
mock.assert_not_called()


def test_middleware_requests_error(client: APIClient, mocker: MockerFixture):
mock = mocker.patch("apitally.client.base.RequestCounter.add_request")

Expand All @@ -92,19 +101,30 @@ def test_middleware_requests_error(client: APIClient, mocker: MockerFixture):
mock.assert_called_once()
assert mock.call_args is not None
assert mock.call_args.kwargs["method"] == "PUT"
assert mock.call_args.kwargs["path"] == "baz/"
assert mock.call_args.kwargs["path"] == "/baz/"
assert mock.call_args.kwargs["status_code"] == 500
assert mock.call_args.kwargs["response_time"] > 0


def test_get_app_info():
from django.urls import get_resolver

from apitally.django import _extract_views_from_url_patterns, _get_app_info
from apitally.django import _get_app_info

views = _extract_views_from_url_patterns(get_resolver().url_patterns)
app_info = _get_app_info(views=views, app_version="1.2.3")
app_info = _get_app_info(app_version="1.2.3", urlconfs=[None])
openapi = json.loads(app_info["openapi"])
assert len(app_info["paths"]) == 4
assert len(openapi["paths"]) == 4

assert app_info["versions"]["django"]
assert app_info["versions"]["djangorestframework"]
assert app_info["versions"]["app"] == "1.2.3"
assert app_info["client"] == "python:django"


def test_get_drf_api_endpoints():
from apitally.django import _get_drf_paths

endpoints = _get_drf_paths([None])
assert len(endpoints) == 4
assert endpoints[0]["method"] == "GET"
assert endpoints[0]["path"] == "/foo/"
assert endpoints[1]["path"] == "/foo/{bar}/"

0 comments on commit 1fa4cd2

Please sign in to comment.