django-ninja-aio-crud 2.0.0rc7__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/.github/workflows/coverage.yml +1 -1
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/.github/workflows/docs.yml +50 -19
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/PKG-INFO +2 -2
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/api/authentication.md +1 -178
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/api/views/api_view.md +24 -15
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/api/views/api_view_set.md +92 -10
- django_ninja_aio_crud-2.2.0/docs/api/views/mixins.md +147 -0
- django_ninja_aio_crud-2.2.0/docs/auth.md +225 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/index.md +11 -15
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/mkdocs.yml +5 -1
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/api.py +24 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/auth.py +121 -3
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/helpers/api.py +5 -3
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/types.py +3 -1
- django_ninja_aio_crud-2.2.0/ninja_aio/views/__init__.py +3 -0
- django_ninja_aio_crud-2.0.0rc7/ninja_aio/views.py → django_ninja_aio_crud-2.2.0/ninja_aio/views/api.py +144 -46
- django_ninja_aio_crud-2.2.0/ninja_aio/views/mixins.py +275 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/pyproject.toml +1 -1
- django_ninja_aio_crud-2.2.0/tests/__init__.py +1 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/helpers/test_many_to_many_api.py +1 -2
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/test_app/models.py +3 -1
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/test_app/views.py +53 -1
- django_ninja_aio_crud-2.2.0/tests/test_auth.py +124 -0
- django_ninja_aio_crud-2.2.0/tests/views/test_views.py +99 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/views/test_viewset.py +231 -1
- django_ninja_aio_crud-2.0.0rc7/tests/views/__init__.py +0 -0
- django_ninja_aio_crud-2.0.0rc7/tests/views/test_views.py +0 -57
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/.gitignore +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/LICENSE +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/README.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/api/models/model_serializer.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/api/views/decorators.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/examples/ex_1/models.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/examples/ex_1/urls.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/examples/ex_1/views.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/examples/ex_2/auth.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/examples/ex_2/models.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/examples/ex_2/urls.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/examples/ex_2/views.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/main.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/decorators.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/models.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/ninja_aio/schemas/helpers.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests → django_ninja_aio_crud-2.2.0/tests/core}/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests/core → django_ninja_aio_crud-2.2.0/tests/generics}/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests/generics → django_ninja_aio_crud-2.2.0/tests/helpers}/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests/helpers → django_ninja_aio_crud-2.2.0/tests/models}/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests/models → django_ninja_aio_crud-2.2.0/tests/test_app}/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests/test_app → django_ninja_aio_crud-2.2.0/tests/views}/__init__.py +0 -0
|
@@ -7,7 +7,7 @@ on:
|
|
|
7
7
|
description: 'Docs version to deploy (e.g. v1.0.0)'
|
|
8
8
|
required: true
|
|
9
9
|
default: 'dev'
|
|
10
|
-
type: choice
|
|
10
|
+
type: choice
|
|
11
11
|
options:
|
|
12
12
|
- dev
|
|
13
13
|
- stable
|
|
@@ -16,12 +16,23 @@ on:
|
|
|
16
16
|
make_latest:
|
|
17
17
|
description: 'Set as "latest" and default?'
|
|
18
18
|
type: boolean
|
|
19
|
-
default:
|
|
19
|
+
default: false
|
|
20
20
|
|
|
21
21
|
delete_version:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
description: 'Docs version to delete'
|
|
23
|
+
required: false
|
|
24
|
+
default: ''
|
|
25
|
+
type: choice
|
|
26
|
+
options:
|
|
27
|
+
- ''
|
|
28
|
+
- dev
|
|
29
|
+
- stable
|
|
30
|
+
- "1.0"
|
|
31
|
+
- "2.0"
|
|
32
|
+
delete_confirm:
|
|
33
|
+
description: 'Confirm deletion of the selected version'
|
|
34
|
+
type: boolean
|
|
35
|
+
default: false
|
|
25
36
|
|
|
26
37
|
permissions:
|
|
27
38
|
contents: write
|
|
@@ -29,8 +40,6 @@ permissions:
|
|
|
29
40
|
jobs:
|
|
30
41
|
deploy:
|
|
31
42
|
runs-on: ubuntu-latest
|
|
32
|
-
|
|
33
|
-
# Only trusted events (SonarQube safe)
|
|
34
43
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
35
44
|
|
|
36
45
|
steps:
|
|
@@ -52,18 +61,15 @@ jobs:
|
|
|
52
61
|
- name: Compute VERSION and MAKE_LATEST
|
|
53
62
|
id: vars
|
|
54
63
|
env:
|
|
55
|
-
# Assign untrusted input to env var FIRST (SonarQube compliant)
|
|
56
64
|
INPUT_VERSION: ${{ inputs.docs_version }}
|
|
57
65
|
shell: bash
|
|
58
66
|
run: |
|
|
59
|
-
# Now use safe shell variable $INPUT_VERSION
|
|
60
67
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "$INPUT_VERSION" ]; then
|
|
61
68
|
VERSION="$INPUT_VERSION"
|
|
62
69
|
else
|
|
63
70
|
VERSION="dev"
|
|
64
71
|
fi
|
|
65
72
|
|
|
66
|
-
# Boolean input is always safe ("true"/"false")
|
|
67
73
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
68
74
|
MAKE_LATEST="${{ inputs.make_latest }}"
|
|
69
75
|
else
|
|
@@ -82,29 +88,54 @@ jobs:
|
|
|
82
88
|
git fetch --all --tags
|
|
83
89
|
git fetch origin gh-pages --depth=1 || true
|
|
84
90
|
|
|
85
|
-
# Idempotent worktree setup
|
|
86
91
|
if git worktree list | grep -q " gh-pages "; then
|
|
87
92
|
git worktree remove gh-pages --force
|
|
88
93
|
fi
|
|
89
94
|
git worktree add gh-pages gh-pages 2>/dev/null || git worktree add gh-pages origin/gh-pages
|
|
90
95
|
|
|
91
96
|
if [ "$MAKE_LATEST" = "true" ]; then
|
|
92
|
-
echo "
|
|
97
|
+
echo "Deploying $VERSION as latest and default"
|
|
93
98
|
mike deploy --push --update-aliases "$VERSION" latest --ignore-remote-status
|
|
94
|
-
mike set-default
|
|
99
|
+
mike set-default --push latest
|
|
95
100
|
else
|
|
96
|
-
echo "
|
|
101
|
+
echo "Deploying $VERSION (non-latest)"
|
|
97
102
|
mike deploy --push "$VERSION" --ignore-remote-status
|
|
98
103
|
fi
|
|
99
104
|
|
|
100
105
|
git worktree remove gh-pages --force
|
|
101
106
|
|
|
102
|
-
- name: Delete version
|
|
103
|
-
if: inputs.delete_version != ''
|
|
107
|
+
- name: Delete version (safe)
|
|
108
|
+
if: github.event_name == 'workflow_dispatch' && inputs.delete_version != '' && inputs.delete_confirm == true
|
|
109
|
+
env:
|
|
110
|
+
DELETE_VERSION: ${{ inputs.delete_version }}
|
|
111
|
+
shell: bash
|
|
104
112
|
run: |
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
echo "
|
|
113
|
+
set -euo pipefail
|
|
114
|
+
|
|
115
|
+
echo "Requested delete: $DELETE_VERSION"
|
|
116
|
+
|
|
117
|
+
# Protect aliases and default
|
|
118
|
+
if [ "$DELETE_VERSION" = "latest" ] || [ "$DELETE_VERSION" = "stable" ]; then
|
|
119
|
+
echo "Refusing to delete protected alias: $DELETE_VERSION"
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# Load current default; fail if attempting to delete it
|
|
124
|
+
CURRENT_DEFAULT="$(mike default || true)"
|
|
125
|
+
if [ -n "$CURRENT_DEFAULT" ] && [ "$DELETE_VERSION" = "$CURRENT_DEFAULT" ]; then
|
|
126
|
+
echo "Refusing to delete current default docs version: $DELETE_VERSION"
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Ensure version exists before deleting
|
|
131
|
+
if ! mike list | awk '{print $1}' | grep -Fxq "$DELETE_VERSION"; then
|
|
132
|
+
echo "Version '$DELETE_VERSION' not found. Nothing to delete."
|
|
133
|
+
exit 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
echo "Deleting version: $DELETE_VERSION"
|
|
137
|
+
mike delete "$DELETE_VERSION" --push
|
|
138
|
+
echo "Version '$DELETE_VERSION' deleted successfully"
|
|
108
139
|
|
|
109
140
|
- name: List versions
|
|
110
141
|
run: mike list
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-ninja-aio-crud
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Django Ninja AIO CRUD - Rest Framework
|
|
5
5
|
Author: Giuseppe Casillo
|
|
6
|
-
Requires-Python: >=3.10
|
|
6
|
+
Requires-Python: >=3.10, <=3.14
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Classifier: Topic :: Internet
|
|
@@ -614,188 +614,11 @@ Support both JWT and API Key:
|
|
|
614
614
|
class ArticleViewSet(APIViewSet):
|
|
615
615
|
model = Article
|
|
616
616
|
api = api
|
|
617
|
-
auth = [JWTAuth()
|
|
617
|
+
auth = [JWTAuth(), APIKeyAuth()] # Either JWT or API Key
|
|
618
618
|
```
|
|
619
619
|
|
|
620
620
|
Django Ninja will try both methods; if either succeeds, the request is authenticated.
|
|
621
621
|
|
|
622
|
-
## Token Validation
|
|
623
|
-
|
|
624
|
-
### Expiration Validation
|
|
625
|
-
|
|
626
|
-
JWT tokens include `exp` claim (expiration timestamp):
|
|
627
|
-
|
|
628
|
-
```python
|
|
629
|
-
# Token payload
|
|
630
|
-
{
|
|
631
|
-
"sub": "123",
|
|
632
|
-
"exp": 1704067200, # Unix timestamp
|
|
633
|
-
"iat": 1704063600
|
|
634
|
-
}
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
AsyncJwtBearer automatically validates expiration:
|
|
638
|
-
|
|
639
|
-
```python
|
|
640
|
-
class JWTAuth(AsyncJwtBearer):
|
|
641
|
-
jwt_public = jwk.RSAKey.import_key(PUBLIC_KEY)
|
|
642
|
-
# Expiration checked automatically
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
**Error Response:**
|
|
646
|
-
|
|
647
|
-
```json
|
|
648
|
-
{
|
|
649
|
-
"detail": "Token has expired"
|
|
650
|
-
}
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
### Not Before Validation
|
|
654
|
-
|
|
655
|
-
Use `nbf` claim for tokens that become valid in the future:
|
|
656
|
-
|
|
657
|
-
```python
|
|
658
|
-
# Token payload
|
|
659
|
-
{
|
|
660
|
-
"sub": "123",
|
|
661
|
-
"nbf": 1704063600, # Not valid before this time
|
|
662
|
-
"exp": 1704067200
|
|
663
|
-
}
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
Automatically validated by AsyncJwtBearer.
|
|
667
|
-
|
|
668
|
-
### Custom Validation
|
|
669
|
-
|
|
670
|
-
```python
|
|
671
|
-
class StrictAuth(AsyncJwtBearer):
|
|
672
|
-
jwt_public = jwk.RSAKey.import_key(PUBLIC_KEY)
|
|
673
|
-
|
|
674
|
-
async def auth_handler(self, request):
|
|
675
|
-
# Check token type
|
|
676
|
-
token_type = self.dcd.claims.get("typ")
|
|
677
|
-
if token_type != "access":
|
|
678
|
-
return False
|
|
679
|
-
|
|
680
|
-
# Check IP whitelist
|
|
681
|
-
allowed_ips = self.dcd.claims.get("allowed_ips", [])
|
|
682
|
-
client_ip = request.META.get('REMOTE_ADDR')
|
|
683
|
-
if allowed_ips and client_ip not in allowed_ips:
|
|
684
|
-
return False
|
|
685
|
-
|
|
686
|
-
# Continue with normal auth
|
|
687
|
-
user_id = self.dcd.claims.get("sub")
|
|
688
|
-
return await User.objects.aget(id=user_id)
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
## Testing Authentication
|
|
692
|
-
|
|
693
|
-
### Unit Tests
|
|
694
|
-
|
|
695
|
-
```python
|
|
696
|
-
import pytest
|
|
697
|
-
from ninja.testing import TestAsyncClient
|
|
698
|
-
from myapp.views import api
|
|
699
|
-
from myapp.auth import JWTAuth
|
|
700
|
-
import jwt
|
|
701
|
-
from datetime import datetime, timedelta
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
def create_token(user_id: int, **claims) -> str:
|
|
705
|
-
payload = {
|
|
706
|
-
"sub": str(user_id),
|
|
707
|
-
"exp": datetime.utcnow() + timedelta(hours=1),
|
|
708
|
-
"iat": datetime.utcnow(),
|
|
709
|
-
**claims
|
|
710
|
-
}
|
|
711
|
-
return jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
@pytest.mark.asyncio
|
|
715
|
-
async def test_authenticated_request():
|
|
716
|
-
client = TestAsyncClient(api)
|
|
717
|
-
|
|
718
|
-
# Create user
|
|
719
|
-
user = await User.objects.acreate(username="testuser")
|
|
720
|
-
|
|
721
|
-
# Create token
|
|
722
|
-
token = create_token(user.id)
|
|
723
|
-
|
|
724
|
-
# Make authenticated request
|
|
725
|
-
response = await client.get(
|
|
726
|
-
"/article/",
|
|
727
|
-
headers={"Authorization": f"Bearer {token}"}
|
|
728
|
-
)
|
|
729
|
-
|
|
730
|
-
assert response.status_code == 200
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
@pytest.mark.asyncio
|
|
734
|
-
async def test_missing_token():
|
|
735
|
-
client = TestAsyncClient(api)
|
|
736
|
-
|
|
737
|
-
response = await client.get("/article/")
|
|
738
|
-
assert response.status_code == 401
|
|
739
|
-
assert "detail" in response.json()
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
@pytest.mark.asyncio
|
|
743
|
-
async def test_expired_token():
|
|
744
|
-
client = TestAsyncClient(api)
|
|
745
|
-
|
|
746
|
-
# Create expired token
|
|
747
|
-
payload = {
|
|
748
|
-
"sub": "123",
|
|
749
|
-
"exp": datetime.utcnow() - timedelta(hours=1), # Expired
|
|
750
|
-
"iat": datetime.utcnow() - timedelta(hours=2)
|
|
751
|
-
}
|
|
752
|
-
token = jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")
|
|
753
|
-
|
|
754
|
-
response = await client.get(
|
|
755
|
-
"/article/",
|
|
756
|
-
headers={"Authorization": f"Bearer {token}"}
|
|
757
|
-
)
|
|
758
|
-
|
|
759
|
-
assert response.status_code == 401
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
@pytest.mark.asyncio
|
|
763
|
-
async def test_invalid_signature():
|
|
764
|
-
client = TestAsyncClient(api)
|
|
765
|
-
|
|
766
|
-
# Create token with wrong key
|
|
767
|
-
payload = {"sub": "123", "exp": datetime.utcnow() + timedelta(hours=1)}
|
|
768
|
-
token = jwt.encode(payload, "wrong-secret", algorithm="HS256")
|
|
769
|
-
|
|
770
|
-
response = await client.get(
|
|
771
|
-
"/article/",
|
|
772
|
-
headers={"Authorization": f"Bearer {token}"}
|
|
773
|
-
)
|
|
774
|
-
|
|
775
|
-
assert response.status_code == 401
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
### Mock Authentication
|
|
779
|
-
|
|
780
|
-
For testing without real tokens:
|
|
781
|
-
|
|
782
|
-
```python
|
|
783
|
-
from unittest.mock import AsyncMock, patch
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
@pytest.mark.asyncio
|
|
787
|
-
@patch('myapp.auth.JWTAuth.auth_handler')
|
|
788
|
-
async def test_with_mock_auth(mock_auth):
|
|
789
|
-
# Mock auth to return test user
|
|
790
|
-
user = await User.objects.acreate(username="testuser")
|
|
791
|
-
mock_auth.return_value = user
|
|
792
|
-
|
|
793
|
-
client = TestAsyncClient(api)
|
|
794
|
-
response = await client.get("/article/")
|
|
795
|
-
|
|
796
|
-
assert response.status_code == 200
|
|
797
|
-
```
|
|
798
|
-
|
|
799
622
|
## Best Practices
|
|
800
623
|
|
|
801
624
|
1. **Use RSA (asymmetric) keys for production:**
|
|
@@ -81,15 +81,12 @@ Registers all defined views to the API instance.
|
|
|
81
81
|
|
|
82
82
|
**Returns:** The router instance
|
|
83
83
|
|
|
84
|
-
**
|
|
85
|
-
|
|
86
|
-
```python
|
|
87
|
-
view = UserAPIView()
|
|
88
|
-
view.add_views_to_route()
|
|
89
|
-
```
|
|
84
|
+
**Note:** When using `@api.view(prefix="/path", tags=[...])`, manual registration via `add_views_to_route()` is not required; the router is mounted automatically.
|
|
90
85
|
|
|
91
86
|
## Complete Example
|
|
92
87
|
|
|
88
|
+
**Recommended:**
|
|
89
|
+
|
|
93
90
|
```python
|
|
94
91
|
from ninja_aio import NinjaAIO
|
|
95
92
|
from ninja_aio.views import APIView
|
|
@@ -101,6 +98,23 @@ class StatsSchema(Schema):
|
|
|
101
98
|
total: int
|
|
102
99
|
active: int
|
|
103
100
|
|
|
101
|
+
@api.view(prefix="/analytics", tags=["Analytics"])
|
|
102
|
+
class AnalyticsView(APIView):
|
|
103
|
+
def views(self):
|
|
104
|
+
@self.router.get("/dashboard", response=StatsSchema)
|
|
105
|
+
async def dashboard(request):
|
|
106
|
+
return {"total": 1000, "active": 750}
|
|
107
|
+
|
|
108
|
+
@self.router.post("/track")
|
|
109
|
+
async def track_event(request, event: str):
|
|
110
|
+
return {"tracked": event}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Alternative implementation:**
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
api = NinjaAIO(title="My API")
|
|
117
|
+
|
|
104
118
|
class AnalyticsView(APIView):
|
|
105
119
|
api = api
|
|
106
120
|
router_tag = "Analytics"
|
|
@@ -109,26 +123,21 @@ class AnalyticsView(APIView):
|
|
|
109
123
|
def views(self):
|
|
110
124
|
@self.router.get("/dashboard", response=StatsSchema)
|
|
111
125
|
async def dashboard(request):
|
|
112
|
-
return {
|
|
113
|
-
"total": 1000,
|
|
114
|
-
"active": 750
|
|
115
|
-
}
|
|
126
|
+
return {"total": 1000, "active": 750}
|
|
116
127
|
|
|
117
128
|
@self.router.post("/track")
|
|
118
129
|
async def track_event(request, event: str):
|
|
119
|
-
# tracking logic
|
|
120
130
|
return {"tracked": event}
|
|
121
131
|
|
|
122
|
-
# Register views
|
|
123
132
|
AnalyticsView().add_views_to_route()
|
|
124
133
|
```
|
|
125
134
|
|
|
126
135
|
## Notes
|
|
127
136
|
|
|
128
137
|
- Use `APIView` for simple, non-CRUD endpoints
|
|
129
|
-
- For CRUD operations, use [`APIViewSet`](api_view_set.md)
|
|
130
|
-
- All views are
|
|
131
|
-
-
|
|
138
|
+
- For CRUD operations, use [`APIViewSet`](api_view_set.md)
|
|
139
|
+
- All views are async-compatible
|
|
140
|
+
- Standard error codes are available via `self.error_codes`
|
|
132
141
|
|
|
133
142
|
Note:
|
|
134
143
|
|
{django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.2.0}/docs/api/views/api_view_set.md
RENAMED
|
@@ -16,7 +16,7 @@ Notes:
|
|
|
16
16
|
|
|
17
17
|
- Retrieve path has no trailing slash; update/delete include a trailing slash.
|
|
18
18
|
- `{base}` auto-resolves from model verbose name plural (lowercase) unless `api_route_path` is provided.
|
|
19
|
-
- Error responses may use a unified generic schema for codes: 400, 401, 404
|
|
19
|
+
- Error responses may use a unified generic schema for codes: 400, 401, 404.
|
|
20
20
|
|
|
21
21
|
## Core Attributes
|
|
22
22
|
|
|
@@ -317,7 +317,7 @@ All generated handlers are decorated with `@unique_view(...)` to ensure stable u
|
|
|
317
317
|
|
|
318
318
|
## Error Handling
|
|
319
319
|
|
|
320
|
-
All CRUD and M2M endpoints may respond with `GenericMessageSchema` for error codes: 400 (validation), 401 (auth), 404 (not found)
|
|
320
|
+
All CRUD and M2M endpoints may respond with `GenericMessageSchema` for error codes: 400 (validation), 401 (auth), 404 (not found).
|
|
321
321
|
|
|
322
322
|
## Performance Tips
|
|
323
323
|
|
|
@@ -329,7 +329,31 @@ All CRUD and M2M endpoints may respond with `GenericMessageSchema` for error cod
|
|
|
329
329
|
|
|
330
330
|
## Minimal Usage
|
|
331
331
|
|
|
332
|
+
Recommended:
|
|
333
|
+
|
|
332
334
|
```python
|
|
335
|
+
from ninja_aio import NinjaAIO
|
|
336
|
+
from ninja_aio.views import APIViewSet
|
|
337
|
+
from .models import User
|
|
338
|
+
|
|
339
|
+
api = NinjaAIO(title="My API")
|
|
340
|
+
|
|
341
|
+
@api.viewset(model=User)
|
|
342
|
+
class UserViewSet(APIViewSet):
|
|
343
|
+
pass
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Note: prefix and tags are optional. If omitted, the base path is inferred from the model verbose name plural and tags default to the model verbose name.
|
|
347
|
+
|
|
348
|
+
Alternative implementation:
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
from ninja_aio import NinjaAIO
|
|
352
|
+
from ninja_aio.views import APIViewSet
|
|
353
|
+
from .models import User
|
|
354
|
+
|
|
355
|
+
api = NinjaAIO(title="My API")
|
|
356
|
+
|
|
333
357
|
class UserViewSet(APIViewSet):
|
|
334
358
|
model = User
|
|
335
359
|
api = api
|
|
@@ -340,18 +364,16 @@ UserViewSet().add_views_to_route()
|
|
|
340
364
|
## Disable Selected Views
|
|
341
365
|
|
|
342
366
|
```python
|
|
367
|
+
@api.viewset(model=User)
|
|
343
368
|
class ReadOnlyUserViewSet(APIViewSet):
|
|
344
|
-
model = User
|
|
345
|
-
api = api
|
|
346
369
|
disable = ["create", "update", "delete"]
|
|
347
370
|
```
|
|
348
371
|
|
|
349
372
|
## Authentication Example
|
|
350
373
|
|
|
351
374
|
```python
|
|
375
|
+
@api.viewset(model=User)
|
|
352
376
|
class UserViewSet(APIViewSet):
|
|
353
|
-
model = User
|
|
354
|
-
api = api
|
|
355
377
|
auth = [JWTAuth()] # global fallback
|
|
356
378
|
get_auth = None # list/retrieve public
|
|
357
379
|
delete_auth = [AdminAuth()] # delete restricted
|
|
@@ -359,7 +381,16 @@ class UserViewSet(APIViewSet):
|
|
|
359
381
|
|
|
360
382
|
## Complete M2M + Filters Example
|
|
361
383
|
|
|
384
|
+
Recommended:
|
|
385
|
+
|
|
362
386
|
```python
|
|
387
|
+
from ninja_aio import NinjaAIO
|
|
388
|
+
from ninja_aio.views import APIViewSet
|
|
389
|
+
from ninja_aio.models import ModelSerializer
|
|
390
|
+
from django.db import models
|
|
391
|
+
|
|
392
|
+
api = NinjaAIO(title="My API")
|
|
393
|
+
|
|
363
394
|
class Tag(ModelSerializer):
|
|
364
395
|
name = models.CharField(max_length=100)
|
|
365
396
|
class ReadSerializer:
|
|
@@ -371,12 +402,41 @@ class User(ModelSerializer):
|
|
|
371
402
|
class ReadSerializer:
|
|
372
403
|
fields = ["id", "username", "tags"]
|
|
373
404
|
|
|
405
|
+
@api.viewset(model=User)
|
|
406
|
+
class UserViewSet(APIViewSet):
|
|
407
|
+
query_params = {"search": (str, None)}
|
|
408
|
+
m2m_relations = [
|
|
409
|
+
M2MRelationSchema(
|
|
410
|
+
model=Tag,
|
|
411
|
+
related_name="tags",
|
|
412
|
+
filters={"name": (str, "")},
|
|
413
|
+
add=True,
|
|
414
|
+
remove=True,
|
|
415
|
+
get=True,
|
|
416
|
+
)
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
async def query_params_handler(self, queryset, filters):
|
|
420
|
+
if filters.get("search"):
|
|
421
|
+
from django.db.models import Q
|
|
422
|
+
s = filters["search"]
|
|
423
|
+
return queryset.filter(Q(username__icontains=s))
|
|
424
|
+
return queryset
|
|
425
|
+
|
|
426
|
+
async def tags_query_params_handler(self, queryset, filters):
|
|
427
|
+
name_filter = filters.get("name")
|
|
428
|
+
if name_filter:
|
|
429
|
+
queryset = queryset.filter(name__icontains=name_filter)
|
|
430
|
+
return queryset
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Alternative implementation:
|
|
434
|
+
|
|
435
|
+
```python
|
|
374
436
|
class UserViewSet(APIViewSet):
|
|
375
437
|
model = User
|
|
376
438
|
api = api
|
|
377
|
-
query_params = {
|
|
378
|
-
"search": (str, None)
|
|
379
|
-
}
|
|
439
|
+
query_params = {"search": (str, None)}
|
|
380
440
|
m2m_relations = [
|
|
381
441
|
M2MRelationSchema(
|
|
382
442
|
model=Tag,
|
|
@@ -384,7 +444,7 @@ class UserViewSet(APIViewSet):
|
|
|
384
444
|
filters={"name": (str, "")},
|
|
385
445
|
add=True,
|
|
386
446
|
remove=True,
|
|
387
|
-
get=True
|
|
447
|
+
get=True,
|
|
388
448
|
)
|
|
389
449
|
]
|
|
390
450
|
|
|
@@ -400,6 +460,28 @@ class UserViewSet(APIViewSet):
|
|
|
400
460
|
if name_filter:
|
|
401
461
|
queryset = queryset.filter(name__icontains=name_filter)
|
|
402
462
|
return queryset
|
|
463
|
+
|
|
464
|
+
UserViewSet().add_views_to_route()
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## ReadOnlyViewSet
|
|
468
|
+
|
|
469
|
+
ReadOnlyViewSet enables only list and retrieve endpoints.
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
@api.viewset(model=MyModel)
|
|
473
|
+
class MyModelReadOnlyViewSet(ReadOnlyViewSet):
|
|
474
|
+
pass
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## WriteOnlyViewSet
|
|
478
|
+
|
|
479
|
+
WriteOnlyViewSet enables only create, update, and delete endpoints.
|
|
480
|
+
|
|
481
|
+
```python
|
|
482
|
+
@api.viewset(model=MyModel)
|
|
483
|
+
class MyModelWriteOnlyViewSet(WriteOnlyViewSet):
|
|
484
|
+
pass
|
|
403
485
|
```
|
|
404
486
|
|
|
405
487
|
## See Also
|