pax_global_header 0000666 0000000 0000000 00000000064 15156602544 0014522 g ustar 00root root 0000000 0000000 52 comment=0b67d47637a9e831287447a6691d1c8c37a6eccb
vitalik-django-ninja-0b67d47/ 0000775 0000000 0000000 00000000000 15156602544 0016061 5 ustar 00root root 0000000 0000000 vitalik-django-ninja-0b67d47/.dockerignore 0000664 0000000 0000000 00000000102 15156602544 0020526 0 ustar 00root root 0000000 0000000 *.pyc
.venv*
.vscode
.mypy_cache
.coverage
htmlcov
dist
test.py
vitalik-django-ninja-0b67d47/.github/ 0000775 0000000 0000000 00000000000 15156602544 0017421 5 ustar 00root root 0000000 0000000 vitalik-django-ninja-0b67d47/.github/FUNDING.yml 0000664 0000000 0000000 00000000113 15156602544 0021231 0 ustar 00root root 0000000 0000000 # polar: django-ninja
custom: ["https://www.buymeacoffee.com/djangoninja"]
vitalik-django-ninja-0b67d47/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15156602544 0021604 5 ustar 00root root 0000000 0000000 vitalik-django-ninja-0b67d47/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001066 15156602544 0024301 0 ustar 00root root 0000000 0000000 ---
name: Bug report
about: Create a report to help us improve
title: "[BUG] "
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Versions (please complete the following information):**
- Python version: [e.g. 3.6]
- Django version: [e.g. 4.0]
- Django-Ninja version: [e.g. 0.16.2]
- Pydantic version: [e.g. 1.9.0]
Note you can quickly get this by runninng in `./manage.py shell` this line:
```
import django; import pydantic; import ninja; django.__version__; ninja.__version__; pydantic.__version__
```
vitalik-django-ninja-0b67d47/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000000555 15156602544 0025336 0 ustar 00root root 0000000 0000000 ---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
vitalik-django-ninja-0b67d47/.github/ISSUE_TEMPLATE/question.md 0000664 0000000 0000000 00000000406 15156602544 0023775 0 ustar 00root root 0000000 0000000 ---
name: Question
about: Having troubles implementing something ?
title: ''
labels: ''
assignees: ''
---
Please describe what you are trying to achieve
Please include code examples (like models code, schemes code, view function) to help understand the issue
vitalik-django-ninja-0b67d47/.github/dependabot.yml 0000664 0000000 0000000 00000000166 15156602544 0022254 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly
vitalik-django-ninja-0b67d47/.github/workflows/ 0000775 0000000 0000000 00000000000 15156602544 0021456 5 ustar 00root root 0000000 0000000 vitalik-django-ninja-0b67d47/.github/workflows/close-old-issues.yml 0000664 0000000 0000000 00000013730 15156602544 0025377 0 ustar 00root root 0000000 0000000 name: Close Old Issues
on:
workflow_dispatch:
inputs:
days_old:
description: 'Close issues older than N days'
required: true
default: '365'
type: number
dry_run:
description: 'Dry run mode (preview only, do not close)'
required: true
default: true
type: boolean
comment_on_close:
description: 'Add comment when closing issues'
required: true
default: true
type: boolean
jobs:
close-old-issues:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Close or list old issues
uses: actions/github-script@v8
with:
script: |
const daysOld = ${{ inputs.days_old }};
const dryRun = ${{ inputs.dry_run }};
const addComment = ${{ inputs.comment_on_close }};
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
console.log(`Looking for issues older than ${daysOld} days (before ${cutoffDate.toISOString()})`);
console.log(`Dry run mode: ${dryRun ? 'YES (no changes will be made)' : 'NO (issues will be closed)'}`);
console.log('---');
let page = 1;
let issuesClosed = 0;
let issuesToClose = [];
while (true) {
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'created',
direction: 'asc',
per_page: 100,
page: page
});
if (issues.data.length === 0) {
break;
}
for (const issue of issues.data) {
// Skip pull requests
if (issue.pull_request) {
continue;
}
const createdAt = new Date(issue.created_at);
const updatedAt = new Date(issue.updated_at);
// Check if issue is old enough based on last update
if (updatedAt < cutoffDate) {
const daysOldCalculated = Math.floor((Date.now() - updatedAt.getTime()) / (1000 * 60 * 60 * 24));
issuesToClose.push({
number: issue.number,
title: issue.title,
created_at: createdAt.toISOString().split('T')[0],
updated_at: updatedAt.toISOString().split('T')[0],
days_old: daysOldCalculated,
url: issue.html_url
});
}
}
page++;
}
console.log(`\nFound ${issuesToClose.length} issue(s) to close:\n`);
for (const issue of issuesToClose) {
console.log(`#${issue.number}: ${issue.title}`);
console.log(` Created: ${issue.created_at}`);
console.log(` Last updated: ${issue.updated_at} (${issue.days_old} days ago)`);
console.log(` URL: ${issue.url}`);
console.log('');
if (!dryRun) {
try {
// Add a comment before closing (if enabled)
if (addComment) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `This issue has been automatically closed due to inactivity (no updates for ${issue.days_old} days). If you believe this issue is still relevant, please feel free to reopen it or create a new issue.`
});
}
// Close the issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'not_planned'
});
issuesClosed++;
console.log(` ✓ Closed issue #${issue.number}`);
} catch (error) {
console.error(` ✗ Failed to close issue #${issue.number}: ${error.message}`);
}
}
}
console.log('\n---');
if (dryRun) {
console.log(`DRY RUN: Would close ${issuesToClose.length} issue(s)`);
console.log('To actually close these issues, run this workflow again with dry_run set to false');
} else {
console.log(`Successfully closed ${issuesClosed} out of ${issuesToClose.length} issue(s)`);
}
// Set output for summary
core.summary
.addHeading(dryRun ? 'Dry Run Results' : 'Close Old Issues Results')
.addRaw(`**Mode:** ${dryRun ? '🔍 Dry Run (Preview Only)' : '✅ Live Run'}\n`)
.addRaw(`**Cutoff Date:** Issues last updated before ${cutoffDate.toISOString().split('T')[0]}\n`)
.addRaw(`**Days Old Threshold:** ${daysOld} days\n`)
.addRaw(`**Issues ${dryRun ? 'Found' : 'Closed'}:** ${dryRun ? issuesToClose.length : issuesClosed}\n\n`);
if (issuesToClose.length > 0) {
core.summary.addHeading('Issues', 3);
const tableData = issuesToClose.map(issue => [
`#${issue.number}`,
issue.title.substring(0, 80) + (issue.title.length > 80 ? '...' : ''),
issue.updated_at,
`${issue.days_old} days`,
`[View](${issue.url})`
]);
core.summary.addTable([
['Issue', 'Title', 'Last Updated', 'Age', 'Link'],
...tableData
]);
} else {
core.summary.addRaw('\nNo issues found matching the criteria.');
}
await core.summary.write();
vitalik-django-ninja-0b67d47/.github/workflows/docs.yml 0000664 0000000 0000000 00000000353 15156602544 0023132 0 ustar 00root root 0000000 0000000 name: Docs
on:
workflow_dispatch:
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Docs update
run: git push origin master:docs
vitalik-django-ninja-0b67d47/.github/workflows/publish.yml 0000664 0000000 0000000 00000001151 15156602544 0023645 0 ustar 00root root 0000000 0000000 name: Publish
on:
release:
types: [published]
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
- name: Install Flit
run: pip install flit
- name: Install Dependencies
run: flit install --symlink
- name: Publish
env:
# FLIT_USERNAME: ${{ secrets.FLIT_USERNAME }}
# FLIT_PASSWORD: ${{ secrets.FLIT_PASSWORD }}
FLIT_USERNAME: __token__
FLIT_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: flit publish
vitalik-django-ninja-0b67d47/.github/workflows/test.yml 0000664 0000000 0000000 00000001057 15156602544 0023163 0 ustar 00root root 0000000 0000000 name: Test Coverage
on:
push:
branches:
- master
jobs:
test_coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Flit
run: pip install flit "django>=5.1"
- name: Install Dependencies
run: flit install --symlink
- name: Test
run: pytest --cov=ninja --cov-report=xml tests
- name: Coverage
uses: codecov/codecov-action@v4.4.1
vitalik-django-ninja-0b67d47/.github/workflows/test_full.yml 0000664 0000000 0000000 00000005315 15156602544 0024206 0 ustar 00root root 0000000 0000000 name: Full Test
on:
push:
workflow_dispatch:
pull_request:
types: [assigned, opened, synchronize, reopened]
jobs:
test:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
django-version: ['<3.2', '<3.3', '<4.2', '<4.3', '<5.1', '<5.2', '<5.3', '<6.1']
exclude:
- python-version: '3.7'
django-version: '<5.1'
- python-version: '3.8'
django-version: '<5.1'
- python-version: '3.9'
django-version: '<5.1'
- python-version: '3.12'
django-version: '<3.2'
- python-version: '3.12'
django-version: '<3.3'
- python-version: '3.13'
django-version: '<3.2'
- python-version: '3.13'
django-version: '<3.3'
# as of oct 2025 looks like django < 5.2 does not support python 3.14
- python-version: '3.14'
django-version: '<3.2'
- python-version: '3.14'
django-version: '<3.3'
- python-version: '3.14'
django-version: '<4.2'
- python-version: '3.14'
django-version: '<4.3'
- python-version: '3.14'
django-version: '<5.1'
- python-version: '3.14'
django-version: '<5.2'
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install core
run: pip install "Django${{ matrix.django-version }}" "pydantic<3"
- name: Install tests
run: pip install pytest pytest-asyncio pytest-django psycopg2-binary
- name: Test
run: pytest
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install Flit
run: pip install flit "django>=5.2"
- name: Install Dependencies
run: flit install --symlink
- name: Test
run: pytest --cov=ninja
codestyle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install Flit
run: pip install flit
- name: Install Dependencies
run: flit install --symlink
- name: Ruff format
run: ruff format --check ninja tests
- name: Ruff lint
run: ruff check ninja tests
- name: mypy
run: mypy ninja tests/mypy_test.py
vitalik-django-ninja-0b67d47/.gitignore 0000664 0000000 0000000 00000000206 15156602544 0020047 0 ustar 00root root 0000000 0000000 *.pyc
.venv*
.vscode
.mypy_cache
.coverage
htmlcov
/coverage.xml
dist
test.py
docs/site
.DS_Store
.idea
.python-version
*.local.md
vitalik-django-ninja-0b67d47/.pre-commit-config.yaml 0000664 0000000 0000000 00000001033 15156602544 0022337 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
hooks:
- id: check-yaml
# - id: end-of-file-fixer
# - id: trailing-whitespace
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.7.1
hooks:
- id: mypy
additional_dependencies: ["django-stubs", "pydantic"]
exclude: (tests|docs)/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.2
hooks:
- id: ruff-format
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
vitalik-django-ninja-0b67d47/CONTRIBUTING.md 0000664 0000000 0000000 00000002351 15156602544 0020313 0 ustar 00root root 0000000 0000000 # Contributing
Django Ninja uses Flit to build, package and publish the project.
to install it use:
```
pip install flit
```
Once you have it - to install all dependencies required for development and testing use this command:
```
flit install --deps develop --symlink
```
Once done you can check if all works with
```
pytest .
```
or using Makefile:
```
make test
```
Now you are ready to make your contribution
When you're done please make sure you to test your functionality
and check the coverage of your contribution.
```
pytest --cov=ninja --cov-report term-missing tests
```
or using Makefile:
```
make test-cov
```
## Code style
Django Ninja uses `ruff`, and `mypy` for style checks.
Run `pre-commit install` to create a git hook to fix your styles before you commit.
Alternatively, manually check your code with:
```
ruff format --check ninja tests
ruff check ninja tests
mypy ninja
```
or using Makefile:
```
make lint
```
Or reformat your code with:
```
ruff format ninja tests
ruff check ninja tests --fix
```
or using Makefile:
```
make fmt
```
## Docs
Please do not forget to document your contribution
Django Ninja uses `mkdocs`:
```
cd docs/
mkdocs serve
```
and go to browser to see changes in real time
vitalik-django-ninja-0b67d47/LICENSE 0000664 0000000 0000000 00000002076 15156602544 0017073 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright (c) 2025 Vitaliy Kucheryaviy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
vitalik-django-ninja-0b67d47/Makefile 0000664 0000000 0000000 00000001400 15156602544 0017514 0 ustar 00root root 0000000 0000000 .DEFAULT_GOAL := help
.PHONY: help
help:
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: install
install: ## Install dependencies
flit install --deps develop --symlink
.PHONY: lint
lint: ## Run code linters
ruff format --check ninja tests
ruff check ninja tests
mypy ninja
.PHONY: fmt format
fmt format: ## Run code formatters
ruff format ninja tests
ruff check --fix ninja tests
.PHONY: test
test: ## Run tests
pytest .
.PHONY: test-cov
test-cov: ## Run tests with coverage
pytest --cov=ninja --cov-report term-missing tests
.PHONY: docs
docs: ## Serve documentation locally
pip install -r docs/requirements.txt
cd docs && mkdocs serve -a localhost:8090
vitalik-django-ninja-0b67d47/README.md 0000664 0000000 0000000 00000007360 15156602544 0017346 0 ustar 00root root 0000000 0000000 ^ Please read ^
Fast to learn, fast to code, fast to run


[](https://badge.fury.io/py/django-ninja)
[](https://pepy.tech/project/django-ninja)
# Django Ninja - Fast Django REST Framework
**Django Ninja** is a web framework for building APIs with **Django** and Python 3.6+ **type hints**.
**Key features:**
- **Easy**: Designed to be easy to use and intuitive.
- **FAST execution**: Very high performance thanks to **Pydantic** and **async support**.
- **Fast to code**: Type hints and automatic docs lets you focus only on business logic.
- **Standards-based**: Based on the open standards for APIs: **OpenAPI** (previously known as Swagger) and **JSON Schema**.
- **Django friendly**: (obviously) has good integration with the Django core and ORM.
- **Production ready**: Used by multiple companies on live projects (If you use django-ninja and would like to publish your feedback, please email ppr.vitaly@gmail.com).

**Documentation**: https://django-ninja.dev
---
## Installation
```
pip install django-ninja
```
## Usage
In your django project next to urls.py create new `api.py` file:
```Python
from ninja import NinjaAPI
api = NinjaAPI()
@api.get("/add")
def add(request, a: int, b: int):
return {"result": a + b}
```
Now go to `urls.py` and add the following:
```Python hl_lines="3 7"
...
from .api import api
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", api.urls), # <---------- !
]
```
**That's it !**
Now you've just created an API that:
- receives an HTTP GET request at `/api/add`
- takes, validates and type-casts GET parameters `a` and `b`
- decodes the result to JSON
- generates an OpenAPI schema for defined operation
### Interactive API docs
Now go to http://127.0.0.1:8000/api/docs
You will see the automatic interactive API documentation (provided by Swagger UI or Redoc):

## Sponsors
Become a sponsor
## What next?
- Read the full documentation here - https://django-ninja.dev
- To support this project, please give star it on Github. 
- Share it [via Twitter](https://twitter.com/intent/tweet?text=Check%20out%20Django%20Ninja%20-%20Fast%20Django%20REST%20Framework%20-%20https%3A%2F%2Fdjango-ninja.dev)
- If you already using django-ninja, please share your feedback to ppr.vitaly@gmail.com
vitalik-django-ninja-0b67d47/docs/ 0000775 0000000 0000000 00000000000 15156602544 0017011 5 ustar 00root root 0000000 0000000 vitalik-django-ninja-0b67d47/docs/docs/ 0000775 0000000 0000000 00000000000 15156602544 0017741 5 ustar 00root root 0000000 0000000 vitalik-django-ninja-0b67d47/docs/docs/chat.md 0000664 0000000 0000000 00000001265 15156602544 0021206 0 ustar 00root root 0000000 0000000 # Ask AI
Please feel free to share any questions or describe any problems you're encountering. Simply enter your text in the chat, and I'll be happy to assist you.
vitalik-django-ninja-0b67d47/docs/docs/extra.css 0000664 0000000 0000000 00000002403 15156602544 0021575 0 ustar 00root root 0000000 0000000 .doc-module code {
white-space: nowrap;
}
/* Ask AI Button Styles */
.ask-ai-button-container {
display: flex;
align-items: center;
margin-left: auto;
margin-right: 1rem;
}
.ask-ai-button {
background-color: white !important;
color: #4caf50 !important; /* Material green-500 */
padding: 0.4rem 0.5rem !important;
border-radius: 0.25rem;
font-weight: 500;
text-decoration: none;
font-size: 0.9rem;
border: 2px solid #4caf50;
transition: all 0.2s ease-in-out;
white-space: nowrap;
margin-left: 8px;
}
.ask-ai-button:hover {
background-color: #4caf50 !important;
color: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
/* Dark mode support */
[data-md-color-scheme="slate"] .ask-ai-button {
background-color: white !important;
color: #4caf50 !important;
}
[data-md-color-scheme="slate"] .ask-ai-button:hover {
background-color: #4caf50 !important;
color: white !important;
}
/* Responsive adjustments */
@media screen and (max-width: 76.1875em) {
.ask-ai-button-container {
margin-right: 0.5rem;
}
.ask-ai-button {
padding: 0.4rem 0.8rem !important;
font-size: 0.85rem;
}
}
@media screen and (max-width: 60em) {
.ask-ai-button-container {
display: none; /* Hide on mobile to save space */
}
}
vitalik-django-ninja-0b67d47/docs/docs/guides/ 0000775 0000000 0000000 00000000000 15156602544 0021221 5 ustar 00root root 0000000 0000000 vitalik-django-ninja-0b67d47/docs/docs/guides/api-docs.md 0000664 0000000 0000000 00000010632 15156602544 0023244 0 ustar 00root root 0000000 0000000 # API Docs
## OpenAPI docs
Once you configured your Ninja API and started runserver - go to http://127.0.0.1:8000/api/docs
You will see the automatic, interactive API documentation (provided by the OpenAPI / Swagger UI
## CDN vs staticfiles
You are not required to put django ninja to `INSTALLED_APPS`. In that case the interactive UI is hosted by CDN.
To host docs (Js/css) from your own server - just put "ninja" to INSTALLED_APPS - in that case standard django staticfiles mechanics will host it.
## Switch to Redoc
```python
from ninja import Redoc
api = NinjaAPI(docs=Redoc())
```
Then you will see the alternative automatic documentation (provided by Redoc).
## Changing docs display settings
To set some custom settings for Swagger or Redocs you can use `settings` param on the docs class
```python
from ninja import Redoc, Swagger
api = NinjaAPI(docs=Swagger(settings={"persistAuthorization": True}))
...
api = NinjaAPI(docs=Redoc(settings={"disableSearch": True}))
```
Settings reference:
- [Swagger configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/)
- [Redoc configuration](https://redocly.com/docs/api-reference-docs/configuration/functionality/)
## Hiding docs
### Hiding the interactive docs viewer
To hide only the interactive documentation UI (Swagger or Redoc) while keeping the OpenAPI schema accessible, set `docs_url` to `None`:
```python
api = NinjaAPI(docs_url=None)
```
This disables the `/docs` endpoint but the OpenAPI schema remains available at `/openapi.json`. This is useful when you want to:
- Disable the interactive UI but keep the schema for API clients or code generators
- Use external documentation tools that consume the OpenAPI spec
### Disabling the OpenAPI schema endpoint
To disable the OpenAPI schema endpoint, set `openapi_url` to `None`:
```python
api = NinjaAPI(openapi_url=None)
```
This disables the `/openapi.json` endpoint. Since the docs viewer depends on the OpenAPI schema, this also disables the docs viewer - no documentation URLs will be registered.
### Summary
| Configuration | `/openapi.json` | `/docs` | Use Case |
|---------------|-----------------|---------|----------|
| Default | Available | Available | Development |
| `docs_url=None` | Available | Hidden | Hide UI, keep schema for clients |
| `openapi_url=None` | Hidden | Hidden | Completely hide all documentation |
## Protecting docs
To protect docs with authentication (or decorate for some other use case) use `docs_decorator` argument:
```python
from django.contrib.admin.views.decorators import staff_member_required
api = NinjaAPI(docs_decorator=staff_member_required)
```
## Extending OpenAPI Spec with custom attributes
You can extend OpenAPI spec with custom attributes, for example to add `termsOfService`
```python
api = NinjaAPI(
openapi_extra={
"info": {
"termsOfService": "https://example.com/terms/",
}
},
title="Demo API",
description="This is a demo API with dynamic OpenAPI info section"
)
```
## Resolving the doc's url
The url for the api's documentation view can be reversed by referencing the view's name `openapi-view`.
In Python code, for example:
```python
from django.urls import reverse
reverse('api-1.0.0:openapi-view')
>>> '/api/docs'
```
In a Django template, for example:
```Html
API DocsAPI Docs
```
## Creating custom docs viewer
To create your own view for OpenAPI - create a class inherited from DocsBase and overwrite `render_page` method:
```python
from ninja.openapi.docs import DocsBase
class MyDocsViewer(DocsBase):
def render_page(self, request, api):
... # return http response
...
api = NinjaAPI(docs=MyDocsViewer())
```
## Using a custom favicon
The django-ninja OpenAPI docs contain a default favicon, the ninja star.
To use your own, overwrite the `ninja/favicon.html` django template.
```html
{% load static %}
{% block favicons %}
{% endblock %}
```
for more information, see the [Django documentation on overriding templates](https://docs.djangoproject.com/en/5.2/howto/overriding-templates/).
vitalik-django-ninja-0b67d47/docs/docs/guides/async-support.md 0000664 0000000 0000000 00000015070 15156602544 0024375 0 ustar 00root root 0000000 0000000 ## Intro
Since **version 3.1**, Django comes with **async views support**. This allows you run efficient concurrent views that are network and/or IO bound.
```
pip install Django>=3.1 django-ninja
```
Async views work more efficiently when it comes to:
- calling external APIs over the network
- executing/waiting for database queries
- reading/writing from/to disk drives
**Django Ninja** takes full advantage of async views and makes it very easy to work with them.
## Quick example
### Code
Let's take an example. We have an API operation that does some work (currently just sleeps for provided number of seconds) and returns a word:
```python hl_lines="5"
import time
@api.get("/say-after")
def say_after(request, delay: int, word: str):
time.sleep(delay)
return {"saying": word}
```
To make this code asynchronous, all you have to do is add the **`async`** keyword to a function (and use async aware libraries for work processing - in our case we will replace the stdlib `sleep` with `asyncio.sleep`):
```python hl_lines="1 4 5"
import asyncio
@api.get("/say-after")
async def say_after(request, delay: int, word: str):
await asyncio.sleep(delay)
return {"saying": word}
```
### Run
To run this code you need an ASGI server like Uvicorn or Daphne. Let's use Uvicorn for, example:
To install Uvicorn, use:
```
pip install uvicorn
```
Then start the server:
```
uvicorn your_project.asgi:application --reload
```
>
> *Note: replace `your_project` with your project package name*
> *`--reload` flag used to automatically reload server if you do any changes to the code (do not use on production)*
>
!!! note
You can run async views with `manage.py runserver`, but it does not work well with some libraries, so at this time (July 2020) it is recommended to use ASGI servers like Uvicorn or Daphne.
### Test
Go to your browser and open http://127.0.0.1:8000/api/say-after?delay=3&word=hello (**delay=3**)
After a 3-second wait you should see the "hello" message.
Now let's flood this operation with **100 parallel requests**:
```
ab -c 100 -n 100 "http://127.0.0.1:8000/api/say-after?delay=3&word=hello"
```
which will result in something like this:
```
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 1.1 1 4
Processing: 3008 3063 16.2 3069 3082
Waiting: 3008 3062 15.7 3068 3079
Total: 3008 3065 16.3 3070 3083
Percentage of the requests served within a certain time (ms)
50% 3070
66% 3072
75% 3075
80% 3076
90% 3081
95% 3082
98% 3083
99% 3083
100% 3083 (longest request)
```
Based on the numbers, our service was able to handle each of the 100 concurrent requests with just a little overhead.
To achieve the same concurrency with WSGI and sync operations you would need to spin up about 10 workers with 10 threads each!
## Mixing sync and async operations
Keep in mind that you can use **both sync and async operations** in your project, and **Django Ninja** will route it automatically:
```python hl_lines="2 7"
@api.get("/say-sync")
def say_after_sync(request, delay: int, word: str):
time.sleep(delay)
return {"saying": word}
@api.get("/say-async")
async def say_after_async(request, delay: int, word: str):
await asyncio.sleep(delay)
return {"saying": word}
```
## Elasticsearch example
Let's take a real world use case. For this example, let's use the latest version of Elasticsearch that now comes with async support:
```
pip install elasticsearch>=7.8.0
```
And now instead of the `Elasticsearch` class, use the `AsyncElasticsearch` class and `await` the results:
```python hl_lines="2 7 11 12"
from ninja import NinjaAPI
from elasticsearch import AsyncElasticsearch
api = NinjaAPI()
es = AsyncElasticsearch()
@api.get("/search")
async def search(request, q: str):
resp = await es.search(
index="documents",
body={"query": {"query_string": {"query": q}}},
size=20,
)
return resp["hits"]
```
## Using ORM
Currently, certain key parts of Django are not able to operate safely in an async environment, as they have global state that is not coroutine-aware. These parts of Django are classified as “async-unsafe”, and are protected from execution in an async environment. **The ORM** is the main example, but there are other parts that are also protected in this way.
Learn more about async safety here in the official Django docs.
So, if you do this:
```python hl_lines="3"
@api.get("/blog/{post_id}")
async def search(request, post_id: int):
blog = Blog.objects.get(pk=post_id)
...
```
it throws an error. Until the async ORM is implemented, you can use the `sync_to_async()` adapter:
```python hl_lines="1 3 9"
from asgiref.sync import sync_to_async
@sync_to_async
def get_blog(post_id):
return Blog.objects.get(pk=post_id)
@api.get("/blog/{post_id}")
async def search(request, post_id: int):
blog = await get_blog(post_id)
...
```
or even shorter:
```python hl_lines="3"
@api.get("/blog/{post_id}")
async def search(request, post_id: int):
blog = await sync_to_async(Blog.objects.get)(pk=post_id)
...
```
There is a common **GOTCHA**: Django querysets are lazily evaluated (database query happens only when you start iterating), so this will **not** work:
```python
all_blogs = await sync_to_async(Blog.objects.all)()
# it will throw an error later when you try to iterate over all_blogs
...
```
Instead, use evaluation (with `list`):
```python
all_blogs = await sync_to_async(list)(Blog.objects.all())
...
```
Since Django **version 4.1**, Django comes with asynchronous versions of ORM operations.
These eliminate the need to use `sync_to_async` in most cases.
The async operations have the same names as their sync counterparts but are prepended with *a*. So using
the example above, you can rewrite it as:
```python hl_lines="3"
@api.get("/blog/{post_id}")
async def search(request, post_id: int):
blog = await Blog.objects.aget(pk=post_id)
...
```
When working with querysets, use `async for` paired with list comprehension:
```python
all_blogs = [blog async for blog in Blog.objects.all()]
...
```
Learn more about the async ORM interface in the official Django docs.
vitalik-django-ninja-0b67d47/docs/docs/guides/authentication.md 0000664 0000000 0000000 00000014532 15156602544 0024567 0 ustar 00root root 0000000 0000000 # Authentication
## Intro
**Django Ninja** provides several tools to help you deal with authentication and authorization easily, rapidly, in a standard way, and without having to study and learn all the security specifications.
The core concept is that when you describe an API operation, you can define an authentication object.
```python hl_lines="2 7"
{!./src/tutorial/authentication/code001.py!}
```
In this example, the client will only be able to call the `pets` method if it uses Django session authentication (the default is cookie based), otherwise an HTTP-401 error will be returned.
If you need to authorize only a superuser, you can use `from ninja.security import django_auth_superuser` instead.
## Automatic OpenAPI schema
Here's an example where the client, in order to authenticate, needs to pass a header:
`Authorization: Bearer supersecret`
```python hl_lines="4 5 6 7 10"
{!./src/tutorial/authentication/bearer01.py!}
```
Now go to the docs at http://localhost:8000/api/docs.

Now, when you click the **Authorize** button, you will get a prompt to input your authentication token.

When you do test calls, the Authorization header will be passed for every request.
## Global authentication
In case you need to secure **all** methods of your API, you can pass the `auth` argument to the `NinjaAPI` constructor:
```python hl_lines="11 19"
from ninja import NinjaAPI, Form
from ninja.security import HttpBearer
class GlobalAuth(HttpBearer):
def authenticate(self, request, token):
if token == "supersecret":
return token
api = NinjaAPI(auth=GlobalAuth())
# @api.get(...)
# def ...
# @api.post(...)
# def ...
```
And, if you need to overrule some of those methods, you can do that on the operation level again by passing the `auth` argument. In this example, authentication will be disabled for the `/token` operation:
```python hl_lines="19"
{!./src/tutorial/authentication/global01.py!}
```
## Available auth options
### Custom function
The "`auth=`" argument accepts any Callable object. **NinjaAPI** passes authentication only if the callable object returns a value that can be **converted to boolean `True`**. This return value will be assigned to the `request.auth` attribute.
```python hl_lines="1 2 3 6"
{!./src/tutorial/authentication/code002.py!}
```
### API Key
Some API's use API keys for authorization. An API key is a token that a client provides when making API calls to identify itself. The key can be sent in the query string:
```
GET /something?api_key=abcdef12345
```
or as a request header:
```
GET /something HTTP/1.1
X-API-Key: abcdef12345
```
or as a cookie:
```
GET /something HTTP/1.1
Cookie: X-API-KEY=abcdef12345
```
**Django Ninja** comes with built-in classes to help you handle these cases.
#### in Query
```python hl_lines="1 2 5 6 7 8 9 10 11 12"
{!./src/tutorial/authentication/apikey01.py!}
```
In this example we take a token from `GET['api_key']` and find a `Client` in the database that corresponds to this key. The Client instance will be set to the `request.auth` attribute.
Note: **`param_name`** is the name of the GET parameter that will be checked for. If not set, the default of "`key`" will be used.
#### in Header
```python hl_lines="1 4"
{!./src/tutorial/authentication/apikey02.py!}
```
#### in Cookie
```python hl_lines="1 4"
{!./src/tutorial/authentication/apikey03.py!}
```
### Django Session Authentication
**Django Ninja** provides built-in session authentication classes that leverage Django's existing session framework:
#### SessionAuth
Uses Django's default session authentication - authenticates any logged-in user:
```python
from ninja.security import SessionAuth
@api.get("/protected", auth=SessionAuth())
def protected_view(request):
return {"user": request.auth.username}
```
#### SessionAuthSuperUser
Authenticates only users with superuser privileges:
```python
from ninja.security import SessionAuthSuperUser
@api.get("/admin-only", auth=SessionAuthSuperUser())
def admin_view(request):
return {"message": "Hello superuser!"}
```
#### SessionAuthIsStaff
Authenticates users who are either superusers or staff members:
```python
from ninja.security import SessionAuthIsStaff
@api.get("/staff-area", auth=SessionAuthIsStaff())
def staff_view(request):
return {"message": "Hello staff member!"}
```
These authentication classes automatically use Django's `SESSION_COOKIE_NAME` setting and check the user's authentication status through the standard Django session framework.
### HTTP Bearer
```python hl_lines="1 4 5 6 7"
{!./src/tutorial/authentication/bearer01.py!}
```
### HTTP Basic Auth
```python hl_lines="1 4 5 6 7"
{!./src/tutorial/authentication/basic01.py!}
```
## Multiple authenticators
The **`auth`** argument also allows you to pass multiple authenticators:
```python hl_lines="18"
{!./src/tutorial/authentication/multiple01.py!}
```
In this case **Django Ninja** will first check the API key `GET`, and if not set or invalid will check the `header` key.
If both are invalid, it will raise an authentication error to the response.
## Router authentication
Use `auth` argument on Router to apply authenticator to all operations declared in it:
```python
api.add_router("/events/", events_router, auth=BasicAuth())
```
or using router constructor
```python
router = Router(auth=BasicAuth())
```
This overrides any API level authentication. To allow router operations to not use the API-level authentication by default, you can explicitly set the router's `auth=None`.
## Custom exceptions
Raising an exception that has an exception handler will return the response from that handler in
the same way an operation would:
```python hl_lines="1 4"
{!./src/tutorial/authentication/bearer02.py!}
```
## Async authentication
**Django Ninja** has basic support for asynchronous authentication. While the default authentication classes are not async-compatible, you can still define your custom asynchronous authentication callables and pass them in using `auth`.
```python
async def async_auth(request):
...
@api.get("/pets", auth=async_auth)
def pets(request):
...
```
See [Handling errors](errors.md) for more information.
vitalik-django-ninja-0b67d47/docs/docs/guides/decorators.md 0000664 0000000 0000000 00000024411 15156602544 0023712 0 ustar 00root root 0000000 0000000 # Decorators
Django Ninja provides flexible decorator support to wrap your API operations with additional functionality like caching, logging, authentication checks, or any custom logic.
## Understanding Decorator Modes
Django Ninja supports two modes for applying decorators:
### OPERATION Mode (Default)
- Applied **after** Django Ninja's validation
- Wraps the operation function with validated data
- Has access to parsed and validated parameters
- Useful for: business logic, logging with validated data, post-validation checks
### VIEW Mode
- Applied **before** Django Ninja's validation
- Wraps the entire Django view function
- Has access to the raw Django request
- Useful for: caching, rate limiting, Django middleware-like functionality
- Similar to Django's standard view decorators
## Using `@decorate_view`
The `@decorate_view` decorator allows you to apply Django view decorators to individual endpoints.
These decorators are always executed in VIEW mode:
```python
from django.views.decorators.cache import cache_page
from ninja import NinjaAPI
from ninja.decorators import decorate_view
api = NinjaAPI()
@api.get("/cached")
@decorate_view(cache_page(60 * 15)) # Cache for 15 minutes
def cached_endpoint(request):
return {"data": "This response is cached"}
```
You can apply multiple decorators:
```python
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
@api.get("/multi")
@decorate_view(cache_page(300), vary_on_headers("User-Agent"))
def multi_decorated(request):
return {"data": "Multiple decorators applied"}
```
## Using `add_decorator`
The `add_decorator` method allows you to apply decorators to multiple endpoints at once.
By default, they are executed in OPERATION mode; however, you can switch them to VIEW mode.
### Router-Level Decorators
Apply decorators to all endpoints in a router:
```python
from ninja import Router
router = Router()
# Add logging to all operations in this router
def log_operation(func):
def wrapper(request, *args, **kwargs):
print(f"Calling {func.__name__}")
result = func(request, *args, **kwargs)
print(f"Result: {result}")
return result
return wrapper
router.add_decorator(log_operation) # OPERATION mode by default
@router.get("/users")
def list_users(request):
return {"users": ["Alice", "Bob"]}
@router.get("/users/{user_id}")
def get_user(request, user_id: int):
return {"user_id": user_id}
```
### API-Level Decorators
Apply decorators to all endpoints in your entire API:
```python
from ninja import NinjaAPI
api = NinjaAPI()
# Add CORS headers to all responses (VIEW mode)
def cors_headers(func):
def wrapper(request, *args, **kwargs):
response = func(request, *args, **kwargs)
response["Access-Control-Allow-Origin"] = "*"
return response
return wrapper
api.add_decorator(cors_headers, mode="view")
# Now all endpoints will have CORS headers
@api.get("/data")
def get_data(request):
return {"data": "example"}
```
## Practical Examples
### Example 1: Request Timing
```python
import time
from functools import wraps
def timing_decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
start = time.time()
result = func(request, *args, **kwargs)
duration = time.time() - start
if isinstance(result, dict):
result["_timing"] = f"{duration:.3f}s"
return result
return wrapper
router = Router()
router.add_decorator(timing_decorator)
@router.get("/slow")
def slow_endpoint(request):
time.sleep(1)
return {"message": "done"}
# Returns: {"message": "done", "_timing": "1.001s"}
```
### Example 2: Authentication Check (OPERATION mode)
```python
from functools import wraps
def require_feature_flag(flag_name):
def decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if not request.user.has_feature(flag_name):
return {"error": f"Feature {flag_name} not enabled"}
return func(request, *args, **kwargs)
return wrapper
return decorator
router = Router()
router.add_decorator(require_feature_flag("new_api"))
@router.get("/new-feature")
def new_feature(request):
return {"feature": "enabled"}
```
### Example 3: Response Caching (VIEW mode)
```python
from django.core.cache import cache
from functools import wraps
import hashlib
def cache_response(timeout=300):
def decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
# Create cache key from request
cache_key = hashlib.md5(
f"{request.path}{request.GET.urlencode()}".encode()
).hexdigest()
# Try to get from cache
cached = cache.get(cache_key)
if cached:
return cached
# Call the view
response = func(request, *args, **kwargs)
# Cache the response
cache.set(cache_key, response, timeout)
return response
return wrapper
return decorator
router = Router()
router.add_decorator(cache_response(600), mode="view")
```
## Decorator Execution Order
When multiple decorators are applied, they execute in this order:
1. API-level decorators (outermost)
2. Parent router decorators
3. Child router decorators
4. Individual endpoint decorators (innermost)
Be aware that VIEW mode decorators are executed before OPERATION mode decorators.
```python
api = NinjaAPI()
parent_router = Router()
child_router = Router()
api.add_decorator(api_decorator)
parent_router.add_decorator(parent_decorator)
child_router.add_decorator(child_decorator)
@child_router.get("/test")
@decorate_view(endpoint_decorator)
def endpoint(request):
return {"result": "ok"}
parent_router.add_router("/child", child_router)
api.add_router("/parent", parent_router)
# Execution order:
# 1. endpoint_decorator (view)
# 1. api_decorator (operational)
# 2. parent_decorator (operational)
# 3. child_decorator (operational)
# 5. endpoint function
```
## Async Support
Decorators work with both sync and async views. When you have mixed sync/async endpoints in the same router, you need to create universal decorators that handle both cases.
### Universal Decorators for Mixed Sync/Async Routers
When you have a router with both sync and async endpoints, use `asyncio.iscoroutinefunction()` to detect the function type:
```python
import asyncio
from functools import wraps
def universal_decorator(func):
if asyncio.iscoroutinefunction(func):
# Handle async functions
@wraps(func)
async def async_wrapper(request, *args, **kwargs):
# Your async logic here
result = await func(request, *args, **kwargs)
if isinstance(result, dict):
result["decorated"] = True
result["type"] = "async"
return result
return async_wrapper
else:
# Handle sync functions
@wraps(func)
def sync_wrapper(request, *args, **kwargs):
# Your sync logic here
result = func(request, *args, **kwargs)
if isinstance(result, dict):
result["decorated"] = True
result["type"] = "sync"
return result
return sync_wrapper
router = Router()
router.add_decorator(universal_decorator)
@router.get("/async")
async def async_endpoint(request):
await asyncio.sleep(0.1)
return {"endpoint": "async"}
@router.get("/sync")
def sync_endpoint(request):
return {"endpoint": "sync"}
```
### Async-Only Decorators
For routers with only async endpoints, you can use async decorators directly:
```python
def async_timing_decorator(func):
@wraps(func)
async def wrapper(request, *args, **kwargs):
start = time.time()
result = await func(request, *args, **kwargs)
duration = time.time() - start
if isinstance(result, dict):
result["_timing"] = f"{duration:.3f}s"
return result
return wrapper
router = Router()
router.add_decorator(async_timing_decorator)
@router.get("/async")
async def async_endpoint(request):
await asyncio.sleep(1)
return {"message": "async done"}
```
### Sync Decorators on Async Views
You can also use sync decorators on async views by handling coroutines:
```python
def sync_decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
result = func(request, *args, **kwargs)
if asyncio.iscoroutine(result):
# Handle async functions
async def async_wrapper():
actual_result = await result
if isinstance(actual_result, dict):
actual_result["sync_decorated"] = True
return actual_result
return async_wrapper()
else:
# Handle sync functions
if isinstance(result, dict):
result["sync_decorated"] = True
return result
return wrapper
```
## When to Use Each Mode
### Use VIEW Mode When:
- You need access to the raw Django request
- Implementing caching at the HTTP level
- Adding/modifying HTTP headers
- Implementing rate limiting
- Working with Django middleware patterns
### Use OPERATION Mode When:
- You need access to validated/parsed data
- Implementing business logic decorators
- Adding data to responses
- Logging with type-safe parameters
- Post-validation security checks
## Best Practices
1. **Use `functools.wraps`**: Always use `@wraps(func)` to preserve function metadata
2. **Handle mixed sync/async routers**: When your router has both sync and async endpoints, use `asyncio.iscoroutinefunction(func)` to create universal decorators
3. **Choose the right approach for async**:
- **Universal decorators**: Best for mixed routers (detect with `iscoroutinefunction`)
- **Async-only decorators**: Best for async-only routers (simpler, cleaner)
- **Sync decorators with coroutine handling**: Useful for legacy decorators
4. **Be mindful of performance**: Decorators add overhead, especially in VIEW mode
5. **Document side effects**: Clearly document what your decorators modify
6. **Keep decorators focused**: Each decorator should have a single responsibility
7. **Test both sync and async**: When using universal decorators, test both sync and async endpoints
vitalik-django-ninja-0b67d47/docs/docs/guides/errors.md 0000664 0000000 0000000 00000011110 15156602544 0023051 0 ustar 00root root 0000000 0000000 # Handling errors
**Django Ninja** allows you to install custom exception handlers to deal with how you return responses when errors or handled exceptions occur.
## Custom exception handlers
Let's say you are making API that depends on some external service that is designed to be unavailable at some moments. Instead of throwing default 500 error upon exception - you can handle the error and give some friendly response back to the client (to come back later)
To achieve that you need:
1. create some exception (or use existing one)
2. use api.exception_handler decorator
Example:
```python hl_lines="9 10"
api = NinjaAPI()
class ServiceUnavailableError(Exception):
pass
# initializing handler
@api.exception_handler(ServiceUnavailableError)
def service_unavailable(request, exc):
return api.create_response(
request,
{"message": "Please retry later"},
status=503,
)
# some logic that throws exception
@api.get("/service")
def some_operation(request):
if random.choice([True, False]):
raise ServiceUnavailableError()
return {"message": "Hello"}
```
Exception handler function takes 2 arguments:
- **request** - Django http request
- **exc** - actual exception
function must return http response
## Override the default exception handlers
**Django Ninja** registers default exception handlers for the types shown below.
You can register your own handlers with `@api.exception_handler` to override the default handlers.
#### `ninja.errors.AuthenticationError`
Raised when authentication data is not valid
#### `ninja.errors.AuthorizationError`
Raised when authentication data is valid, but doesn't allow you to access the resource
#### `ninja.errors.ValidationError`
Raised when request data does not validate
#### `ninja.errors.HttpError`
Used to throw http error with status code from any place of the code
#### `django.http.Http404`
Django's default 404 exception (can be returned f.e. with `get_object_or_404`)
#### `Exception`
Any other unhandled exception by application.
Default behavior
- **if `settings.DEBUG` is `True`** - returns a traceback in plain text (useful when debugging in console or swagger UI)
- **else** - default django exception handler mechanism is used (error logging, email to ADMINS)
## Customizing request validation errors
Requests that fail validation raise `ninja.errors.ValidationError` (not to be confused with `pydantic.ValidationError`).
`ValidationError`s have a default exception handler that returns a 422 (Unprocessable Content) JSON response of the form:
```json
{
"detail": [ ... ]
}
```
You can change this behavior by overriding the default handler for `ValidationError`s:
```python hl_lines="1 4"
from ninja.errors import ValidationError
...
@api.exception_handler(ValidationError)
def validation_errors(request, exc):
return HttpResponse("Invalid input", status=422)
```
If you need even more control over validation errors (for example, if you need to reference the schema associated with
the model that failed validation), you can supply your own `validation_error_from_error_contexts` in a `NinjaAPI` subclass:
```python hl_lines="4"
from ninja.errors import ValidationError, ValidationErrorContext
from typing import Any, Dict, List
class CustomNinjaAPI(NinjaAPI):
def validation_error_from_error_contexts(
self, error_contexts: List[ValidationErrorContext],
) -> ValidationError:
custom_error_infos: List[Dict[str, Any]] = []
for context in error_contexts:
model = context.model
pydantic_schema = model.__pydantic_core_schema__
param_source = model.__ninja_param_source__
for e in context.pydantic_validation_error.errors(
include_url=False, include_context=False, include_input=False
):
custom_error_info = {
# TODO: use `e`, `param_source`, and `pydantic_schema` as desired
}
custom_error_infos.append(custom_error_info)
return ValidationError(custom_error_infos)
api = CustomNinjaAPI()
```
Now each `ValidationError` raised during request validation will contain data from your `validation_error_from_error_contexts`.
## Throwing HTTP responses with exceptions
As an alternative to custom exceptions and writing handlers for it - you can as well throw http exception that will lead to returning a http response with desired code
```python
from ninja.errors import HttpError
@api.get("/some/resource")
def some_operation(request):
if True:
raise HttpError(503, "Service Unavailable. Please retry later.")
```
vitalik-django-ninja-0b67d47/docs/docs/guides/input/ 0000775 0000000 0000000 00000000000 15156602544 0022360 5 ustar 00root root 0000000 0000000 vitalik-django-ninja-0b67d47/docs/docs/guides/input/body.md 0000664 0000000 0000000 00000011167 15156602544 0023645 0 ustar 00root root 0000000 0000000 # Request Body
Request bodies are typically used with “create” and “update” operations (POST, PUT, PATCH).
For example, when creating a resource using POST or PUT, the request body usually contains the representation of the resource to be created.
To declare a **request body**, you need to use **Django Ninja `Schema`**.
!!! info
Under the hood **Django Ninja** uses Pydantic models with all their power and benefits.
The alias `Schema` was chosen to avoid confusion in code when using Django models, as Pydantic's model class is called Model by default, and conflicts with Django's Model class.
## Import Schema
First, you need to import `Schema` from `ninja`:
```python hl_lines="2"
{!./src/tutorial/body/code01.py!}
```
## Create your data model
Then you declare your data model as a class that inherits from `Schema`.
Use standard Python types for all the attributes:
```python hl_lines="5 6 7 8 9"
{!./src/tutorial/body/code01.py!}
```
Note: if you use **`None`** as the default value for an attribute, it will become optional in the request body.
For example, this model above declares a JSON "`object`" (or Python `dict`) like:
```JSON
{
"name": "Katana",
"description": "An optional description",
"price": 299.00,
"quantity": 10
}
```
...as `description` is optional (with a default value of `None`), this JSON "`object`" would also be valid:
```JSON
{
"name": "Katana",
"price": 299.00,
"quantity": 10
}
```
## Declare it as a parameter
To add it to your *path operation*, declare it the same way you declared the path and query parameters:
```python hl_lines="13"
{!./src/tutorial/body/code01.py!}
```
... and declare its type as the model you created, `Item`.
## Results
With just that Python type declaration, **Django Ninja** will:
* Read the body of the request as JSON.
* Convert the corresponding types (if needed).
* Validate the data.
* If the data is invalid, it will return a nice and meaningful error, indicating exactly where and what the incorrect data was.
* Give you the received data in the parameter `item`.
* Because you declared it in the function to be of type `Item`, you will also have all the editor support
(completion, etc.) for all the attributes and their types.
* Generate JSON Schema definitions for
your models, and you can also use them anywhere else you like if it makes sense for your project.
* Those schemas will be part of the generated OpenAPI schema, and used by the automatic documentation UI's.
## Automatic docs
The JSON Schemas of your models will be part of your OpenAPI generated schema, and will be shown in the interactive API docs:

... and they will be also used in the API docs inside each *path operation* that needs them:

## Editor support
In your editor, inside your function you will get type hints and completion everywhere (this wouldn't happen if you received a `dict` instead of a Schema object):

The previous screenshots were taken with Visual Studio Code.
You would get the same editor support with PyCharm and most of the other Python editors.
## Request body + path parameters
You can declare path parameters **and** body requests at the same time.
**Django Ninja** will recognize that the function parameters that match path parameters should be **taken from the path**, and that function parameters that are declared with `Schema` should be **taken from the request body**.
```python hl_lines="11 12"
{!./src/tutorial/body/code02.py!}
```
## Request body + path + query parameters
You can also declare **body**, **path** and **query** parameters, all at the same time.
**Django Ninja** will recognize each of them and take the data from the correct place.
```python hl_lines="11 12"
{!./src/tutorial/body/code03.py!}
```
The function parameters will be recognized as follows:
* If the parameter is also declared in the **path**, it will be used as a path parameter.
* If the parameter is of a **singular type** (like `int`, `float`, `str`, `bool`, etc.), it will be interpreted as a **query** parameter.
* If the parameter is declared to be of the type of **Schema** (or Pydantic `BaseModel`), it will be interpreted as a request **body**.
vitalik-django-ninja-0b67d47/docs/docs/guides/input/file-params.md 0000664 0000000 0000000 00000010241 15156602544 0025100 0 ustar 00root root 0000000 0000000 # File uploads
Handling files are no different from other parameters.
```python hl_lines="1 2 5"
from ninja import NinjaAPI, File
from ninja.files import UploadedFile
@api.post("/upload")
def upload(request, file: File[UploadedFile]):
data = file.read()
return {'name': file.name, 'len': len(data)}
```
`UploadedFile` is an alias to [Django's UploadFile](https://docs.djangoproject.com/en/stable/ref/files/uploads/#django.core.files.uploadedfile.UploadedFile) and has all the methods and attributes to access the uploaded file:
- read()
- multiple_chunks(chunk_size=None)
- chunks(chunk_size=None)
- name
- size
- content_type
- content_type_extra
- charset
- etc.
## Uploading array of files
To **upload several files** at the same time, just declare a `List` of `UploadedFile`:
```python hl_lines="1 6"
from typing import List
from ninja import NinjaAPI, File
from ninja.files import UploadedFile
@api.post("/upload-many")
def upload_many(request, files: File[List[UploadedFile]]):
return [f.name for f in files]
```
## Uploading files with extra fields
Note: The HTTP protocol does not allow you to send files in `application/json` format by default (unless you encode it somehow to JSON on client side)
To send files along with some extra attributes, you need to send bodies with `multipart/form-data` encoding. You can do it by simply marking fields with `Form`:
```python hl_lines="14"
from ninja import NinjaAPI, Schema, UploadedFile, Form, File
from datetime import date
api = NinjaAPI()
class UserDetails(Schema):
first_name: str
last_name: str
birthdate: date
@api.post('/users')
def create_user(request, details: Form[UserDetails], file: File[UploadedFile]):
return [details.dict(), file.name]
```
Note: in this case all fields should be send as form fields
You can as well send payload in single field as JSON - just remove the Form mark from:
```python
@api.post('/users')
def create_user(request, details: UserDetails, file: File[UploadedFile]):
return [details.dict(), file.name]
```
this will expect from the client side to send data as `multipart/form-data with 2 fields:
- details: JSON as string
- file: file
### List of files with extra info
```python
@api.post('/users')
def create_user(request, details: Form[UserDetails], files: File[list[UploadedFile]]):
return [details.dict(), [f.name for f in files]]
```
### Optional file input
If you would like the file input to be optional, all that you have to do is to pass `None` to the `File` type, like so:
```python
@api.post('/users')
def create_user(request, details: Form[UserDetails], avatar: File[UploadedFile] = None):
user = add_user_to_database(details)
if avatar is not None:
set_user_avatar(user)
```
## Handling request.FILES in PUT/PATCH Requests
**Problem**
```python
@api.put("/upload") # !!!!
def upload(request, file: File[UploadedFile]):
...
```
For some [historical reasons Django’s](https://groups.google.com/g/django-users/c/BeBKj_6qNsc) `request.FILES` is populated only for POST requests by default. When using HTTP PUT or PATCH methods with file uploads (e.g., multipart/form-data), request.FILES will not contain uploaded files. This is a known Django behavior, not specific to Django Ninja.
As a result, views expecting files in PUT or PATCH requests may not behave correctly, since request.FILES will be empty.
**Solution**
Django Ninja provides a built-in middleware to automatically fix this behavior:
`ninja.compatibility.files.fix_request_files_middleware`
This middleware will manually parse multipart/form-data for PUT and PATCH requests and populate request.FILES, making file uploads work as expected across all HTTP methods.
**Usage**
To enable the middleware, add the following to your Django settings:
```python
MIDDLEWARE = [
# ... your existing middleware ...
"ninja.compatibility.files.fix_request_files_middleware",
]
```
**Auto-detection**
When Django Ninja detects a PUT or PATCH etc methods with multipart/form-data and expected FILES - it will throw an error message suggesting you install the compatibility middleware:
Note: This middleware does not interfere with normal POST behavior or any other methods.
vitalik-django-ninja-0b67d47/docs/docs/guides/input/filtering.md 0000664 0000000 0000000 00000021140 15156602544 0024663 0 ustar 00root root 0000000 0000000 # Filtering
If you want to allow the user to filter your querysets by a number of different attributes, it makes sense
to encapsulate your filters into a `FilterSchema` class. `FilterSchema` is a regular `Schema`, so it's using all the
necessary features of Pydantic, but it also adds some bells and whistles that ease the translation of the user-facing filtering
parameters into database queries.
Start off with defining a subclass of `FilterSchema`:
```python hl_lines="6 7 8 9"
from ninja import FilterSchema
from typing import Optional
from datetime import datetime
class BookFilterSchema(FilterSchema):
name: Optional[str] = None
author: Optional[str] = None
created_after: Optional[datetime] = None
```
Next, use this schema in conjunction with `Query` in your API handler:
```python hl_lines="2"
@api.get("/books")
def list_books(request, filters: BookFilterSchema = Query(...)):
books = Book.objects.all()
books = filters.filter(books)
return books
```
Just like described in [defining query params using schema](./query-params.md#using-schema), Django Ninja converts the fields
defined in `BookFilterSchema` into query parameters.
You can use a shorthand one-liner `.filter()` to apply those filters to your queryset:
```python hl_lines="4"
@api.get("/books")
def list_books(request, filters: Query[BookFilterSchema]):
books = Book.objects.all()
books = filters.filter(books)
return books
```
Under the hood, `FilterSchema` converts its fields into [Q expressions](https://docs.djangoproject.com/en/3.1/topics/db/queries/#complex-lookups-with-q-objects) which it then combines and uses to filter your queryset.
Alternatively to using the `.filter` method, you can get the prepared `Q`-expression and perform the filtering yourself.
That can be useful, when you have some additional queryset filtering on top of what you expose to the user through the API:
```python hl_lines="5 8"
@api.get("/books")
def list_books(request, filters: Query[BookFilterSchema]):
# Never serve books from inactive publishers and authors
q = Q(author__is_active=True) | Q(publisher__is_active=True)
# But allow filtering the rest of the books
q &= filters.get_filter_expression()
return Book.objects.filter(q)
```
By default, the filters will behave the following way:
* `None` values will be ignored and not filtered against;
* Every non-`None` field will be converted into a `Q`-expression based on the `Field` definition of each field;
* All `Q`-expressions will be merged into one using `AND` logical operator;
* The resulting `Q`-expression is used to filter the queryset and return you a queryset with a `.filter` clause applied.
## Customizing Fields
By default, `FilterSet` will use the field names to generate Q expressions:
```python
class BookFilterSchema(FilterSchema):
name: Optional[str] = None
```
The `name` field will be converted into `Q(name=...)` expression.
When your database lookups are more complicated than that, you can annotate your fields with an instance of `FilterLookup` where you specify how you wish your field to be looked up for filtering:
```python hl_lines="5"
from ninja import FilterSchema, FilterLookup
from typing import Annotated
class BookFilterSchema(FilterSchema):
name: Annotated[Optional[str], FilterLookup("name__icontains")] = None
```
You can even specify multiple lookups as a list:
```python hl_lines="3 4 5"
class BookFilterSchema(FilterSchema):
search: Annotated[Optional[str], FilterLookup(
["name__icontains",
"author__name__icontains",
"publisher__name__icontains"]
)]
```
By default, field-level expressions are combined using `"OR"` connector, so with the above setup, a query parameter `?search=foobar` will search for books that have "foobar" in either of their name, author or publisher.
And to make generic fields, you can make the field name implicit by skipping it:
```python hl_lines="1 4"
IContainsField = Annotated[Optional[str], FilterLookup('__icontains')]
class BookFilterSchema(FilterSchema):
name: IContainsField = None
```
??? note "Deprecated syntax"
In previous versions, database lookups were specified using `Field(q=...)` syntax:
```python
from ninja import FilterSchema, Field
class BookFilterSchema(FilterSchema):
name: Optional[str] = Field(None, q="name__icontains")
```
This approach is still supported, but it is considered **deprecated** and **not recommended** for new code because:
- Poor IDE support (IDEs don't recognize custom `Field` arguments)
- Uses deprecated Pydantic features (`**extra`)
- Less type-safe and harder to maintain
The new `FilterLookup` annotation provides better developer experience with full IDE support and type safety. Prefer using `FilterLookup` for new projects.
## Combining expressions
By default,
* Field-level expressions are joined together using `OR` operator.
* The fields themselves are joined together using `AND` operator.
So, with the following `FilterSchema`...
```python
class BookFilterSchema(FilterSchema):
search: Annotated[
Optional[str],
FilterLookup(["name__icontains", "author__name__icontains"])] = None
popular: Optional[bool] = None
```
...and the following query parameters from the user
```
http://localhost:8000/api/books?search=harry&popular=true
```
the `FilterSchema` instance will look for popular books that have `harry` in the book's _or_ author's name.
You can customize this behavior using an `expression_connector` argument in field-level and class-level definition:
```python hl_lines="12"
from ninja import FilterConfigDict, FilterLookup, FilterSchema
class BookFilterSchema(FilterSchema):
active: Annotated[
Optional[bool],
FilterLookup(
["is_active", "publisher__is_active"],
expression_connector="AND"
)] = None
name: Annotated[Optional[str], FilterLookup("name__icontains")] = None
model_config = FilterConfigDict(expression_connector="OR")
```
An expression connector can take the values of `"OR"`, `"AND"` and `"XOR"`, but the latter is only [supported](https://docs.djangoproject.com/en/4.1/ref/models/querysets/#xor) in Django starting with 4.1.
Now, a request with these query parameters
```
http://localhost:8000/api/books?name=harry&active=true
```
...shall search for books that have `harry` in their name _or_ are active themselves _and_ are published by active publishers.
## Filtering by Nones
You can make the `FilterSchema` treat `None` as a valid value that should be filtered against.
This can be done on a field level with a `ignore_none` kwarg:
```python hl_lines="3"
class BookFilterSchema(FilterSchema):
name: Annotated[Optional[str], FilterLookup("name__icontains")] = None
tag: Annotated[Optional[str], FilterLookup("tag", ignore_none=False)] = None
```
This way when no other value for `"tag"` is provided by the user, the filtering will always include a condition `tag=None`.
You can also specify this setting for all fields at the same time in `model_config`:
```python hl_lines="5"
class BookFilterSchema(FilterSchema):
name: Annotated[Optional[str], FilterLookup("name__icontains")] = None
tag: Optional[str] = None
model_config = FilterConfigDict(ignore_none=False)
```
## Custom expressions
Sometimes you might want to have complex filtering scenarios that cannot be handled by individual Field annotations.
For such cases you can implement your field filtering logic as a custom method. Simply define a method called `filter_` which takes a filter value and returns a Q expression:
```python hl_lines="5"
class BookFilterSchema(FilterSchema):
tag: Optional[str] = None
popular: Optional[bool] = None
def filter_popular(self, value: bool) -> Q:
return Q(view_count__gt=1000) | Q(download_count__gt=100) if value else Q()
```
Such field methods take precedence over what is specified in the `Field()` definition of the corresponding fields.
If that is not enough, you can implement your own custom filtering logic for the entire `FilterSet` class in a `custom_expression` method:
```python hl_lines="5"
class BookFilterSchema(FilterSchema):
name: Optional[str] = None
popular: Optional[bool] = None
def custom_expression(self) -> Q:
q = Q()
if self.name:
q &= Q(name__icontains=self.name)
if self.popular:
q &= (
Q(view_count__gt=1000) |
Q(downloads__gt=100) |
Q(tag='popular')
)
return q
```
The `custom_expression` method takes precedence over any other definitions described earlier, including `filter_` methods.
vitalik-django-ninja-0b67d47/docs/docs/guides/input/form-params.md 0000664 0000000 0000000 00000003443 15156602544 0025132 0 ustar 00root root 0000000 0000000 # Form data
**Django Ninja** also allows you to parse and validate `request.POST` data
(aka `application/x-www-form-urlencoded` or `multipart/form-data`).
## Form Data as params
```python hl_lines="1 4"
from ninja import NinjaAPI, Form
@api.post("/login")
def login(request, username: Form[str], password: Form[str]):
return {'username': username, 'password': '*****'}
```
Note the following:
1) You need to import the `Form` class from `ninja`
```python
from ninja import Form
```
2) Use `Form` as default value for your parameter:
```python
username: Form[str]
```
## Using a Schema
In a similar manner to [Body](body.md#declare-it-as-a-parameter), you can use
a Schema to organize your parameters.
```python hl_lines="12"
{!./src/tutorial/form/code01.py!}
```
## Request form + path + query parameters
In a similar manner to [Body](body.md#request-body-path-query-parameters), you can use
Form data in combination with other parameter sources.
You can declare query **and** path **and** form field, **and** etc... parameters at the same time.
**Django Ninja** will recognize that the function parameters that match path
parameters should be **taken from the path**, and that function parameters that
are declared with `Form(...)` should be **taken from the request form fields**, etc.
```python hl_lines="12"
{!./src/tutorial/form/code02.py!}
```
## Mapping Empty Form Field to Default
Form fields that are optional, are often sent with an empty value. This value is
interpreted as an empty string, and thus may fail validation for fields such as `int` or `bool`.
This can be fixed, as described in the Pydantic docs, by using
[Generic Classes as Types](https://pydantic-docs.helpmanual.io/usage/types/#generic-classes-as-types).
```python hl_lines="15 16 23-25"
{!./src/tutorial/form/code03.py!}
```
vitalik-django-ninja-0b67d47/docs/docs/guides/input/operations.md 0000664 0000000 0000000 00000002361 15156602544 0025067 0 ustar 00root root 0000000 0000000 # HTTP Methods
## Defining operations
An `operation` can be one of the following [HTTP methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods):
- GET
- POST
- PUT
- DELETE
- PATCH
**Django Ninja** comes with a decorator for each operation:
```python hl_lines="1 5 9 13 17"
@api.get("/path")
def get_operation(request):
...
@api.post("/path")
def post_operation(request):
...
@api.put("/path")
def put_operation(request):
...
@api.delete("/path")
def delete_operation(request):
...
@api.patch("/path")
def patch_operation(request):
...
```
See the [operations parameters](../../reference/operations-parameters.md)
reference docs for information on what you can pass to any of these decorators.
## Handling multiple methods
If you need to handle multiple methods with a single function for a given path,
you can use the `api_operation` decorator:
```python hl_lines="1"
@api.api_operation(["POST", "PATCH"], "/path")
def mixed_operation(request):
...
```
This feature can also be used to implement other HTTP methods that don't have
corresponding **Django Ninja** methods, such as `HEAD` or `OPTIONS`.
```python hl_lines="1"
@api.api_operation(["HEAD", "OPTIONS"], "/path")
def mixed_operation(request):
...
```
vitalik-django-ninja-0b67d47/docs/docs/guides/input/path-params.md 0000664 0000000 0000000 00000010061 15156602544 0025115 0 ustar 00root root 0000000 0000000 # Path parameters
You can declare path "parameters" with the same syntax used by Python format-strings (which luckily also matches the OpenAPI path parameters):
```python hl_lines="1 2"
{!./src/tutorial/path/code01.py!}
```
The value of the path parameter `item_id` will be passed to your function as the argument `item_id`.
So, if you run this example and go to http://localhost:8000/api/items/foo, you will see this response:
```JSON
{"item_id":"foo"}
```
### Path parameters with types
You can declare the type of path parameter in the function using standard Python type annotations:
```python hl_lines="2"
{!./src/tutorial/path/code02.py!}
```
In this case,`item_id` is declared to be an **`int`**. This will give you editor and linter support for error checks, completion, etc.
If you run this in your browser with http://localhost:8000/api/items/3, you will see this response:
```JSON
{"item_id":3}
```
!!! tip
Notice that the value your function received (and returned) is **3**, as a Python `int` - not a string `"3"`.
So, with just that type declaration, **Django Ninja** gives you automatic request "parsing" and validation.
### Data validation
On the other hand, if you go to the browser at http://localhost:8000/api/items/foo *(`"foo"` is not int)*, you will see an HTTP error like this:
```JSON hl_lines="8"
{
"detail": [
{
"loc": [
"path",
"item_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
```
### Django Path Converters
You can use [Django Path Converters](https://docs.djangoproject.com/en/stable/topics/http/urls/#path-converters)
to help parse the path:
```python hl_lines="1"
@api.get("/items/{int:item_id}")
def read_item(request, item_id):
return {"item_id": item_id}
```
In this case,`item_id` will be parsed as an **`int`**. If `item_id` is not a valid `int`, the url will not
match. (e.g. if no other path matches, a *404 Not Found* will be returned)
!!! tip
Notice that, since **Django Ninja** uses a default type of `str` for unannotated parameters, the value the
function above received (and returned) is `"3"`, as a Python `str` - not an integer **3**. To receive
an `int`, simply declare `item_id` as an `int` type annotation in the function definition as normal:
```python hl_lines="2"
@api.get("/items/{int:item_id}")
def read_item(request, item_id:int):
return {"item_id": item_id}
```
#### Path params with slashes
Django's `path` converter allows you to handle path-like parameters:
```python hl_lines="1"
@api.get('/dir/{path:value}')
def someview(request, value: str):
return value
```
you can query this operation with `/dir/some/path/with-slashes` and your `value` will be equal to `some/path/with-slashes`
### Multiple parameters
You can pass as many variables as you want into `path`, just remember to have unique names and don't forget to use the same names in the function arguments.
```python
@api.get("/events/{year}/{month}/{day}")
def events(request, year: int, month: int, day: int):
return {"date": [year, month, day]}
```
### Using Schema
You can also use Schema to encapsulate path parameters that depend on each other (and validate them as a group):
```python hl_lines="1 2 5 6 7 8 9 10 11 15"
{!./src/tutorial/path/code010.py!}
```
!!! note
Notice that here we used a `Path` source hint to let **Django Ninja** know that this schema will be applied to path parameters.
### Documentation
Now, when you open your browser at http://localhost:8000/api/docs, you will see the automatic, interactive, API documentation.

vitalik-django-ninja-0b67d47/docs/docs/guides/input/query-params.md 0000664 0000000 0000000 00000006010 15156602544 0025325 0 ustar 00root root 0000000 0000000 # Query parameters
When you declare other function parameters that are not part of the path parameters, they are automatically interpreted as "query" parameters.
```python hl_lines="5"
{!./src/tutorial/query/code01.py!}
```
To query this operation, you use a URL like:
```
http://localhost:8000/api/weapons?offset=0&limit=10
```
By default, all GET parameters are strings, and when you annotate your function arguments with types, they are converted to that type and validated against it.
The same benefits that apply to path parameters also apply to query parameters:
- Editor support (obviously)
- Data "parsing"
- Data validation
- Automatic documentation
!!! Note
if you do not annotate your arguments, they will be treated as `str` types
```python hl_lines="2"
@api.get("/weapons")
def list_weapons(request, limit, offset):
# type(limit) == str
# type(offset) == str
```
### Defaults
As query parameters are not a fixed part of a path, they are optional and can have default values:
```python hl_lines="2"
@api.get("/weapons")
def list_weapons(request, limit: int = 10, offset: int = 0):
return weapons[offset : offset + limit]
```
In the example above we set default values of `offset=0` and `limit=10`.
So, going to the URL:
```
http://localhost:8000/api/weapons
```
would be the same as going to:
```
http://localhost:8000/api/weapons?offset=0&limit=10
```
If you go to, for example:
```
http://localhost:8000/api/weapons?offset=20
```
the parameter values in your function will be:
- `offset=20` (because you set it in the URL)
- `limit=10` (because that was the default value)
### Required and optional parameters
You can declare required or optional GET parameters in the same way as declaring Python function arguments:
```python hl_lines="5"
{!./src/tutorial/query/code02.py!}
```
In this case, **Django Ninja** will always validate that you pass the `q` param in the GET, and the `offset` param is an optional integer.
### GET parameters type conversion
Let's declare multiple type arguments:
```python hl_lines="5"
{!./src/tutorial/query/code03.py!}
```
The `str` type is passed as is.
For the `bool` type, all the following:
```
http://localhost:8000/api/example?b=1
http://localhost:8000/api/example?b=True
http://localhost:8000/api/example?b=true
http://localhost:8000/api/example?b=on
http://localhost:8000/api/example?b=yes
```
or any other case variation (uppercase, first letter in uppercase, etc.), your function will see
the parameter `b` with a `bool` value of `True`, otherwise as `False`.
Date can be both date string and integer (unix timestamp):
http://localhost:8000/api/example?d=1577836800 # same as 2020-01-01
http://localhost:8000/api/example?d=2020-01-01
### Using Schema
You can also use Schema to encapsulate GET parameters:
```python hl_lines="1 2 5 6 7 8"
{!./src/tutorial/query/code010.py!}
```
For more complex filtering scenarios please refer to [filtering](./filtering.md).
vitalik-django-ninja-0b67d47/docs/docs/guides/input/request-parsers.md 0000664 0000000 0000000 00000003276 15156602544 0026057 0 ustar 00root root 0000000 0000000 # Request parsers
In most cases, the default content type for REST API's is JSON, but in case you need to work with
other content types (like YAML, XML, CSV) or use faster JSON parsers, **Django Ninja** provides a `parser` configuration.
```python
api = NinjaAPI(parser=MyYamlParser())
```
To create your own parser, you need to extend the `ninja.parser.Parser` class, and override the `parse_body` method.
## Example YAML Parser
Let's create our custom YAML parser:
```python hl_lines="4 8 9"
import yaml
from typing import List
from ninja import NinjaAPI
from ninja.parser import Parser
class MyYamlParser(Parser):
def parse_body(self, request):
return yaml.safe_load(request.body)
api = NinjaAPI(parser=MyYamlParser())
class Payload(Schema):
ints: List[int]
string: str
f: float
@api.post('/yaml')
def operation(request, payload: Payload):
return payload.dict()
```
If you now send YAML like this as the request body:
```YAML
ints:
- 0
- 1
string: hello
f: 3.14
```
it will be correctly parsed, and you should have JSON output like this:
```JSON
{
"ints": [
0,
1
],
"string": "hello",
"f": 3.14
}
```
## Example ORJSON Parser
[orjson](https://github.com/ijl/orjson#orjson) is a fast, accurate JSON library for Python. It benchmarks as the fastest Python library for JSON and is more accurate than the standard `json` library or other third-party libraries.
```
pip install orjson
```
Parser code:
```python hl_lines="1 8 9"
import orjson
from ninja import NinjaAPI
from ninja.parser import Parser
class ORJSONParser(Parser):
def parse_body(self, request):
return orjson.loads(request.body)
api = NinjaAPI(parser=ORJSONParser())
```
vitalik-django-ninja-0b67d47/docs/docs/guides/response/ 0000775 0000000 0000000 00000000000 15156602544 0023057 5 ustar 00root root 0000000 0000000 vitalik-django-ninja-0b67d47/docs/docs/guides/response/config-pydantic.md 0000664 0000000 0000000 00000003637 15156602544 0026470 0 ustar 00root root 0000000 0000000 # Overriding Pydantic Config
There are many customizations available for a **Django Ninja `Schema`**, via the schema's
[Pydantic `model_config`](https://docs.pydantic.dev/latest/api/config/).
!!! info
Under the hood **Django Ninja** uses [Pydantic Models](https://pydantic-docs.helpmanual.io/usage/models/)
with all their power and benefits. The alias `Schema` was chosen to avoid confusion in code
when using Django models, as Pydantic's model class is called Model by default, and conflicts with
Django's Model class.
## Example Camel Case mode
One interesting config attribute is [`alias_generator`](https://docs.pydantic.dev/latest/api/config/?query=alias_generator#pydantic.config.ConfigDict.alias_generator).
Using Pydantic's example in **Django Ninja** can look something like:
```python hl_lines="10"
from pydantic import ConfigDict
from ninja import Schema
def to_camel(string: str) -> str:
words = string.split('_')
return words[0].lower() + ''.join(word.capitalize() for word in words[1:])
class CamelModelSchema(Schema):
model_config = ConfigDict(alias_generator=to_camel)
str_field_name: str
float_field_name: float
```
Keep in mind that when you want modify output for field names (like camel case) - you need to set as well `populate_by_name` and `by_alias`
```python hl_lines="6 14"
from pydantic import ConfigDict
class UserSchema(ModelSchema):
model_config = ConfigDict(
alias_generator = to_camel
populate_by_name = True, # !!!!!! <--------
)
class Meta:
model = User
fields = ["id", "email", "is_staff"]
@api.get("/users", response=list[UserSchema], by_alias=True) # !!!!!! <-------- by_alias
def get_users(request):
return User.objects.all()
```
results:
```JSON
[
{
"id": 1,
"email": "tim@apple.com",
"isStaff": true
},
{
"id": 2,
"email": "sarah@smith.com",
"isStaff": false
}
...
]
```
vitalik-django-ninja-0b67d47/docs/docs/guides/response/django-pydantic-create-schema.md 0000664 0000000 0000000 00000005474 15156602544 0031165 0 ustar 00root root 0000000 0000000 # Using create_schema
Under the hood, [`ModelSchema`](django-pydantic.md#modelschema) uses the `create_schema` function.
This is a more advanced (and less safe) method - please use it carefully.
## `create_schema`
**Django Ninja** comes with a helper function `create_schema`:
```python
def create_schema(
model, # django model
name = "", # name for the generated class, if empty model names is used
depth = 0, # if > 0 schema will also be created for the nested ForeignKeys and Many2Many (with the provided depth of lookup)
fields: list[str] = None, # if passed - ONLY these fields will added to schema
exclude: list[str] = None, # if passed - these fields will be excluded from schema
optional_fields: list[str] | str = None, # if passed - these fields will not be required on schema (use '__all__' to mark ALL fields required)
custom_fields: list[tuple(str, Any, Any)] = None, # if passed - this will override default field types (or add new fields)
)
```
Take this example:
```python hl_lines="2 4"
from django.contrib.auth.models import User
from ninja.orm import create_schema
UserSchema = create_schema(User)
# Will create schema like this:
#
# class UserSchema(Schema):
# id: int
# username: str
# first_name: str
# last_name: str
# password: str
# last_login: datetime
# is_superuser: bool
# email: str
# ... and the rest
```
!!! Warning
By default `create_schema` builds a schema with ALL model fields.
This can lead to accidental unwanted data exposure (like hashed password, in the above example).
**Always** use `fields` or `exclude` arguments to explicitly define list of attributes.
### Using `fields`
```python hl_lines="1"
UserSchema = create_schema(User, fields=['id', 'username'])
# Will create schema like this:
#
# class UserSchema(Schema):
# id: int
# username: str
```
### Using `exclude`
```python hl_lines="1 2"
UserSchema = create_schema(User, exclude=[
'password', 'last_login', 'is_superuser', 'is_staff', 'groups', 'user_permissions']
)
# Will create schema without excluded fields:
#
# class UserSchema(Schema):
# id: int
# username: str
# first_name: str
# last_name: str
# email: str
# is_active: bool
# date_joined: datetime
```
### Using `depth`
The `depth` argument allows you to introspect the Django model into the Related fields(ForeignKey, OneToOne, ManyToMany).
```python hl_lines="1 7"
UserSchema = create_schema(User, depth=1, fields=['username', 'groups'])
# Will create the following schema:
#
# class UserSchema(Schema):
# username: str
# groups: List[Group]
```
Note here that groups became a `List[Group]` - many2many field introspected 1 level deeper and created schema as well for group:
```python
class Group(Schema):
id: int
name: str
permissions: List[int]
```
vitalik-django-ninja-0b67d47/docs/docs/guides/response/django-pydantic.md 0000664 0000000 0000000 00000011402 15156602544 0026452 0 ustar 00root root 0000000 0000000 # Schemas from Django models
Schemas are very useful to define your validation rules and responses, but sometimes you need to reflect your database models into schemas and keep changes in sync.
## ModelSchema
`ModelSchema` is a special base class that can automatically generate schemas from your models.
All you need is to set `model` and `fields` attributes on your schema `Meta`:
```python hl_lines="2 5 6 7"
from django.contrib.auth.models import User
from ninja import ModelSchema
class UserSchema(ModelSchema):
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name']
# Will create schema like this:
#
# class UserSchema(Schema):
# id: int
# username: str
# first_name: str
# last_name: str
```
!!! note
You can also specify the model using a [Django Absolute Lazy relationship](https://docs.djangoproject.com/en/stable/ref/models/fields/#absolute).
In the above example, it would be `model = 'auth.User'`.
### Using ALL model fields
To use all fields from a model - you can pass `__all__` to `fields`:
```python hl_lines="4"
class UserSchema(ModelSchema):
class Meta:
model = User
fields = "__all__"
```
!!! Warning
Using __all__ is not recommended.
This can lead to accidental unwanted data exposure (like hashed password, in the above example).
General advice - use `fields` to explicitly define list of fields that you want to be visible in API.
### Excluding model fields
To use all fields **except** a few, you can use `exclude` configuration:
```python hl_lines="4"
class UserSchema(ModelSchema):
class Meta:
model = User
exclude = ['password', 'last_login', 'user_permissions']
# Will create schema like this:
#
# class UserSchema(Schema):
# id: int
# username: str
# first_name: str
# last_name: str
# email: str
# is_superuser: bool
# ... and the rest
```
### Overriding fields
To change default annotation for some field, or to add a new field, just use annotated attributes as usual.
```python hl_lines="1 2 3 4 8"
class GroupSchema(ModelSchema):
class Meta:
model = Group
fields = ['id', 'name']
class UserSchema(ModelSchema):
groups: List[GroupSchema] = []
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name']
```
### Making fields optional
Pretty often for PATCH API operations you need to make all fields of your schema optional. To do that, you can use config fields_optional
```python hl_lines="5"
class PatchGroupSchema(ModelSchema):
class Meta:
model = Group
fields = ['id', 'name', 'description'] # Note: all these fields are required on model level
fields_optional = '__all__'
```
Also, you can define a subset of optional fields instead of `__all__`:
```python
fields_optional = ['description']
```
When you process input data, you need to tell Pydantic to avoid setting undefined fields to `None`:
```python
@api.patch("/patch/{pk}")
def patch(request, pk: int, payload: PatchGroupSchema):
# Notice that we set exclude_unset=True
updated_fields = payload.dict(exclude_unset=True)
obj = MyModel.objects.get(pk=pk)
for attr, value in updated_fields.items():
setattr(obj, attr, value)
obj.save()
```
### Custom fields types
For each Django field it encounters, `ModelSchema` uses the default `Field.get_internal_type` method
to find the correct representation in Pydantic schema (python type). This process works fine for the built-in field
types, but there are cases where the user wants to create or use a custom field, with its own mapping to
python type. In this case you should use `register_field` method to tell django-ninja which type should this django field represent:
```python hl_lines="4 7 8 9"
# models.py
class MyModel(models.Model):
embedding = pgvector.VectorField()
# schemas.py
from ninja.orm import register_field
register_field('VectorField', list[float])
```
#### PatchDict
Another way to work with patch request data is a `PatchDict` container which allows you to make
a schema with all optional fields and get a dict with **only** fields that was provide
```Python hl_lines="1 11"
from ninja import PatchDict
class GroupSchema(Schema):
# You do not have to make fields optional it will be converted by PatchDict
name: str
description: str
due_date: date
@api.patch("/patch/{pk}")
def modify_data(request, pk: int, payload: PatchDict[GroupSchema]):
obj = MyModel.objects.get(pk=pk)
for attr, value in payload.items():
setattr(obj, attr, value)
obj.save()
```
in this example the `payload` argument will be of type `dict` and only contain fields that were passed in the request and validated using `GroupSchema`
vitalik-django-ninja-0b67d47/docs/docs/guides/response/index.md 0000664 0000000 0000000 00000027634 15156602544 0024524 0 ustar 00root root 0000000 0000000 # Response Schema
**Django Ninja** allows you to define the schema of your responses both for validation and documentation purposes.
Imagine you need to create an API operation that creates a user. The **input** parameter would be **username+password**, but **output** of this operation should be **id+username** (**without** the password).
Let's create the input schema:
```python hl_lines="3 5"
from ninja import Schema
class UserIn(Schema):
username: str
password: str
@api.post("/users/")
def create_user(request, data: UserIn):
user = User(username=data.username) # User is django auth.User
user.set_password(data.password)
user.save()
# ... return ?
```
Now let's define the output schema, and pass it as a `response` argument to the `@api.post` decorator:
```python hl_lines="8 9 10 13 18"
from ninja import Schema
class UserIn(Schema):
username: str
password: str
class UserOut(Schema):
id: int
username: str
@api.post("/users/", response=UserOut)
def create_user(request, data: UserIn):
user = User(username=data.username)
user.set_password(data.password)
user.save()
return user
```
**Django Ninja** will use this `response` schema to:
- convert the output data to declared schema
- validate the data
- add an OpenAPI schema definition
- it will be used by the automatic documentation systems
- and, most importantly, it **will limit the output data** only to the fields only defined in the schema.
## Nested objects
There is also often a need to return responses with some nested/child objects.
Imagine we have a `Task` Django model with a `User` ForeignKey:
```python hl_lines="6"
from django.db import models
class Task(models.Model):
title = models.CharField(max_length=200)
is_completed = models.BooleanField(default=False)
owner = models.ForeignKey("auth.User", null=True, blank=True)
```
Now let's output all tasks, and for each task, output some fields about the user.
```python hl_lines="13 16"
from typing import List
from ninja import Schema
class UserSchema(Schema):
id: int
first_name: str
last_name: str
class TaskSchema(Schema):
id: int
title: str
is_completed: bool
owner: UserSchema = None # ! None - to mark it as optional
@api.get("/tasks", response=List[TaskSchema])
def tasks(request):
queryset = Task.objects.select_related("owner")
return list(queryset)
```
If you execute this operation, you should get a response like this:
```JSON hl_lines="6 7 8 9 16"
[
{
"id": 1,
"title": "Task 1",
"is_completed": false,
"owner": {
"id": 1,
"first_name": "John",
"last_name": "Doe",
}
},
{
"id": 2,
"title": "Task 2",
"is_completed": false,
"owner": null
},
]
```
## Aliases
Instead of a nested response, you may want to just flatten the response output.
The Ninja `Schema` object extends Pydantic's `Field(..., alias="")` format to
work with dotted responses.
Using the models from above, let's make a schema that just includes the task
owner's first name inline, and also uses `completed` rather than `is_completed`:
```python hl_lines="1 7-9"
from ninja import Field, Schema
class TaskSchema(Schema):
id: int
title: str
# The first Field param is the default, use ... for required fields.
completed: bool = Field(..., alias="is_completed")
owner_first_name: str = Field(None, alias="owner.first_name")
```
Aliases also support django template syntax variables access:
```python hl_lines="2"
class TaskSchema(Schema):
last_message: str = Field(None, alias="message_set.0.text")
```
```python hl_lines="3"
class TaskSchema(Schema):
type: str = Field(None)
type_display: str = Field(None, alias="get_type_display") # callable will be executed
```
## Resolvers
You can also create calculated fields via resolve methods based on the field
name.
The method must accept a single argument, which will be the object the schema
is resolving against.
When creating a resolver as a standard method, `self` gives you access to other
validated and formatted attributes in the schema.
```python hl_lines="5 7-11"
class TaskSchema(Schema):
id: int
title: str
is_completed: bool
owner: Optional[str] = None
lower_title: str
@staticmethod
def resolve_owner(obj):
if not obj.owner:
return
return f"{obj.owner.first_name} {obj.owner.last_name}"
def resolve_lower_title(self, obj):
return self.title.lower()
```
### Accessing extra context
Pydantic v2 allows you to process an extra context that is passed to the serializer. In the following example you can have resolver that gets request object from passed `context` argument:
```python hl_lines="6"
class Data(Schema):
a: int
path: str = ""
@staticmethod
def resolve_path(obj, context):
request = context["request"]
return request.path
```
if you use this schema for incoming requests - the `request` object will be automatically passed to context.
You can as well pass your own context:
```python
data = Data.model_validate({'some': 1}, context={'request': MyRequest()})
```
## Returning querysets
In the previous example we specifically converted a queryset into a list (and executed the SQL query during evaluation).
You can avoid that and return a queryset as a result, and it will be automatically evaluated to List:
```python hl_lines="3"
@api.get("/tasks", response=List[TaskSchema])
def tasks(request):
return Task.objects.all()
```
!!! warning
If your operation is async, this example will not work because the ORM query needs to be called safely.
```python hl_lines="2"
@api.get("/tasks", response=List[TaskSchema])
async def tasks(request):
return Task.objects.all()
```
See the [async support](../async-support.md#using-orm) guide for more information.
## FileField and ImageField
**Django Ninja** by default converts files and images (declared with `FileField` or `ImageField`) to `string` URL's.
An example:
```python hl_lines="3"
class Picture(models.Model):
title = models.CharField(max_length=100)
image = models.ImageField(upload_to='images')
```
If you need to output to response image field, declare a schema for it as follows:
```python hl_lines="3"
class PictureSchema(Schema):
title: str
image: str
```
Once you output this to a response, the URL will be automatically generated for each object:
```JSON
{
"title": "Zebra",
"image": "/static/images/zebra.jpg"
}
```
## Multiple Response Schemas
Sometimes you need to define more than response schemas.
In case of authentication, for example, you can return:
- **200** successful -> token
- **401** -> Unauthorized
- **402** -> Payment required
- **403** -> Forbidden
- etc..
In fact, the [OpenAPI specification](https://swagger.io/docs/specification/describing-responses/) allows you to pass multiple response schemas.
You can pass to a `response` argument a dictionary where:
- key is a response code
- value is a schema for that code
Also, when you return the result - you have to also pass a status code to tell **Django Ninja** which schema should be used for validation and serialization.
An example:
```python hl_lines="1 9 12 14 16"
from ninja import Status
class Token(Schema):
token: str
expires: date
class Message(Schema):
message: str
@api.post('/login', response={200: Token, 401: Message, 402: Message})
def login(request, payload: Auth):
if auth_not_valid:
return Status(401, {'message': 'Unauthorized'})
if negative_balance:
return Status(402, {'message': 'Insufficient balance amount. Please proceed to a payment page.'})
return Status(200, {'token': xxx, ...})
```
!!! warning "Deprecated: tuple syntax"
Returning `(status_code, body)` tuples is deprecated and will be removed in a future version.
Use `Status(status_code, body)` instead.
## Multiple response codes
In the previous example you saw that we basically repeated the `Message` schema twice:
```
...401: Message, 402: Message}
```
To avoid this duplication you can use multiple response codes for a schema:
```python hl_lines="2 3 6 9 11"
...
from ninja import Status
from ninja.responses import codes_4xx
@api.post('/login', response={200: Token, codes_4xx: Message})
def login(request, payload: Auth):
if auth_not_valid:
return Status(401, {'message': 'Unauthorized'})
if negative_balance:
return Status(402, {'message': 'Insufficient balance amount. Please proceed to a payment page.'})
return Status(200, {'token': xxx, ...})
```
**Django Ninja** comes with the following HTTP codes:
```python
from ninja.responses import codes_1xx
from ninja.responses import codes_2xx
from ninja.responses import codes_3xx
from ninja.responses import codes_4xx
from ninja.responses import codes_5xx
```
You can also create your own range using a `frozenset`:
```python
my_codes = frozenset({416, 418, 425, 429, 451})
@api.post('/login', response={200: Token, my_codes: Message})
def login(request, payload: Auth):
...
```
## Empty responses
Some responses, such as [204 No Content](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204), have no body.
To indicate the response body is empty mark `response` argument with `None` instead of Schema:
```python hl_lines="1 3"
@api.post("/no_content", response={204: None})
def no_content(request):
return Status(204, None)
```
## Error responses
Check [Handling errors](../errors.md) for more information.
## Self-referencing schemes
Sometimes you need to create a schema that has reference to itself, or tree-structure objects.
To do that you need:
- set a type of your schema in quotes
- use `model_rebuild` method to apply self referencing types
```python hl_lines="3 6"
class Organization(Schema):
title: str
part_of: 'Organization' = None #!! note the type in quotes here !!
Organization.model_rebuild() # !!! this is important
@api.get('/organizations', response=List[Organization])
def list_organizations(request):
...
```
## Self-referencing schemes from `create_schema()`
To be able to use the method `model_rebuild()` from a schema generated via `create_schema()`,
the "name" of the class needs to be in our namespace. In this case it is very important to pass
the `name` parameter to `create_schema()`
```python hl_lines="3"
UserSchema = create_schema(
User,
name='UserSchema', # !!! this is important for model_rebuild()
fields=['id', 'username']
custom_fields=[
('manager', 'UserSchema', None),
]
)
UserSchema.model_rebuild()
```
## Serializing Outside of Views
Serialization of your objects can be done directly in code through the use of
the `.from_orm()` method on the schema object.
Consider the following model:
```python
class Person(models.Model):
name = models.CharField(max_length=50)
```
Which can be accessed using this schema:
```python
class PersonSchema(Schema):
name: str
```
Direct serialization can be performed using the `.from_orm()` method on the
schema. Once you have an instance of the schema object, the `.dict()` and
`.json()` methods allow you to get at both dictionary output and string JSON
versions.
```python
>>> person = Person.objects.get(id=1)
>>> data = PersonSchema.from_orm(person)
>>> data
PersonSchema(id=1, name='Mr. Smith')
>>> data.dict()
{'id':1, 'name':'Mr. Smith'}
>>> data.json()
'{"id":1, "name":"Mr. Smith"}'
```
Multiple Items: or a queryset (or list)
```python
>>> persons = Person.objects.all()
>>> data = [PersonSchema.from_orm(i).dict() for i in persons]
[{'id':1, 'name':'Mr. Smith'},{'id': 2, 'name': 'Mrs. Smith'}...]
```
## Django HTTP responses
It is also possible to return regular django http responses:
```python
from django.http import HttpResponse
from django.shortcuts import redirect
@api.get("/http")
def result_django(request):
return HttpResponse('some data') # !!!!
@api.get("/something")
def some_redirect(request):
return redirect("/some-path") # !!!!
```
vitalik-django-ninja-0b67d47/docs/docs/guides/response/pagination.md 0000664 0000000 0000000 00000021156 15156602544 0025537 0 ustar 00root root 0000000 0000000 # Pagination
**Django Ninja** comes with a pagination support. This allows you to split large result sets into individual pages.
To apply pagination to a function - just apply `paginate` decorator:
```python hl_lines="1 4"
from ninja.pagination import paginate
@api.get('/users', response=List[UserSchema])
@paginate
def list_users(request):
return User.objects.all()
```
That's it!
Now you can query users with `limit` and `offset` GET parameters
```
/api/users?limit=10&offset=0
```
by default limit is set to `100` (you can change it in your settings.py using `NINJA_PAGINATION_PER_PAGE`)
## Built in Pagination Classes
### LimitOffsetPagination (default)
This is the default pagination class (You can change it in your settings.py using `NINJA_PAGINATION_CLASS` path to a class)
```python hl_lines="1 4"
from ninja.pagination import paginate, LimitOffsetPagination
@api.get('/users', response=List[UserSchema])
@paginate(LimitOffsetPagination)
def list_users(request):
return User.objects.all()
```
Example query:
```
/api/users?limit=10&offset=0
```
this class has two input parameters:
- `limit` - defines a number of queryset on the page (default = 100, change in NINJA_PAGINATION_PER_PAGE)
- `offset` - set's the page window offset (default: 0, indexing starts with 0)
### PageNumberPagination
```python hl_lines="1 4"
from ninja.pagination import paginate, PageNumberPagination
@api.get('/users', response=List[UserSchema])
@paginate(PageNumberPagination)
def list_users(request):
return User.objects.all()
```
Example query:
```
/api/users?page=2
```
this class has one parameter `page` and outputs 100 queryset per page by default (can be changed with settings.py)
Page numbering start with 1
you can also set custom page_size value individually per view:
```python hl_lines="2"
@api.get("/users")
@paginate(PageNumberPagination, page_size=50)
def list_users(...
```
In addition to the `page` parameter, you can also use the `page_size` parameter to dynamically adjust the number of records displayed per page:
Example query:
```
/api/users?page=2&page_size=20
```
This allows you to temporarily override the page size setting in your request. The request will use the specified `page_size` value if provided. Otherwise, it will use either the value specified in the decorator or the value from `PAGINATION_MAX_PER_PAGE_SIZE` in settings.py if no decorator value is set.
### CursorPagination
Cursor-based pagination provides stable pagination for datasets that may change frequently. Cursor pagination uses base64 encoded tokens to mark positions in the dataset, ensuring consistent results even when items are added or removed.
```python hl_lines="1 4"
from ninja.pagination import paginate, CursorPagination
@api.get('/events', response=List[EventSchema])
@paginate(CursorPagination)
def list_events(request):
return Event.objects.all()
```
Example query:
```
/api/events?cursor=eyJwIjoiMjAyNC0wMS0wMSIsInIiOmZhbHNlLCJvIjowfQ==
```
this class has two input parameters:
- `cursor` - base64 token representing the current position (optional, starts from beginning if not provided)
- `page_size` - number of items per page (optional)
You can specify the `page_size` value to temporarily override in the request:
```
/api/events?cursor=eyJwIjoiMjAyNC0wMS0wMSIsInIiOmZhbHNlLCJvIjowfQ==&page_size=5
```
This class has a few parameters, which determine how the cursor position is ascertained and the parameter encoded:
- `ordering` - tuple of field names to order the queryset. Use `-` prefix for descending order. The first one of which will be used to encode the position. The ordering field should be unique if possible. A string representation of this field will be used to point to the current position of the cursor. Timestamps work well if each item in the collection is created independently. The paginator can handle some non-uniqueness by adding an offset. Defaults to `("-pk",)`, change in `NINJA_PAGINATION_DEFAULT_ORDERING`
- `page_size` - default page size for endpoint. Defaults to `100`, change in `NINJA_PAGINATION_PER_PAGE`
- `max_page_size` - maximum allowed page size for endpoint. Defaults to `100`, change in `NINJA_PAGINATION_MAX_PER_PAGE_SIZE`
Finally, there is a `NINJA_PAGINATION_MAX_OFFSET` setting to limit malicious cursor requests. It defaults to `100`.
The class parameters can be set globally via settings as well as per view:
```python hl_lines="2"
@api.get("/events")
@paginate(CursorPagination, ordering=("start_date", "end_date"), page_size=20, max_page_size=100)
def list_events(request):
return Event.objects.all()
```
The response includes navigation links and results:
```json
{
"next": "http://api.example.com/events?cursor=eyJwIjoiMjAyNC0wMS0wMiIsInIiOmZhbHNlLCJvIjowfQ==",
"previous": "http://api.example.com/events?cursor=eyJwIjoiMjAyNC0wMS0wMSIsInIiOnRydWUsIm8iOjB9",
"results": [
{ "id": 1, "title": "Event 1", "start_date": "2024-01-01" },
{ "id": 2, "title": "Event 2", "start_date": "2024-01-02" }
]
}
```
## Accessing paginator parameters in view function
If you need access to `Input` parameters used for pagination in your view function - use `pass_parameter` argument
In that case input data will be available in `**kwargs`:
```python hl_lines="2 4"
@api.get("/someview")
@paginate(pass_parameter="pagination_info")
def someview(request, **kwargs):
page = kwargs["pagination_info"].page
return ...
```
## Creating Custom Pagination Class
To create a custom pagination class you should subclass `ninja.pagination.PaginationBase` and override the `Input` and `Output` schema classes and `paginate_queryset(self, queryset, request, **params)` method:
- The `Input` schema is a Schema class that describes parameters that should be passed to your paginator (f.e. page-number or limit/offset values).
- The `Output` schema describes schema for page output (f.e. count/next-page/items/etc).
- The `paginate_queryset` method is passed the initial queryset and should return an iterable object that contains only the data in the requested page. This method accepts the following arguments:
- `queryset`: a queryset (or iterable) returned by the api function
- `pagination` - the paginator.Input parameters (parsed and validated)
- `**params`: kwargs that will contain all the arguments that decorated function received
Example:
```python hl_lines="7 11 16 26"
from ninja.pagination import paginate, PaginationBase
from ninja import Schema
class CustomPagination(PaginationBase):
# only `skip` param, defaults to 5 per page
class Input(Schema):
skip: int
class Output(Schema):
items: List[Any] # `items` is a default attribute
total: int
per_page: int
def paginate_queryset(self, queryset, pagination: Input, **params):
skip = pagination.skip
return {
'items': queryset[skip : skip + 5],
'total': queryset.count(),
'per_page': 5,
}
@api.get('/users', response=List[UserSchema])
@paginate(CustomPagination)
def list_users(request):
return User.objects.all()
```
Tip: You can access request object from params:
```python
def paginate_queryset(self, queryset, pagination: Input, **params):
request = params["request"]
```
#### Async Pagination
Standard **Django Ninja** pagination classes support async. If you wish to handle async requests with a custom pagination class, you should subclass `ninja.pagination.AsyncPaginationBase` and override the `apaginate_queryset(self, queryset, request, **params)` method.
### Output attribute
By default page items are placed to `'items'` attribute. To override this behaviour use `items_attribute`:
```python hl_lines="4 8"
class CustomPagination(PaginationBase):
...
class Output(Schema):
results: List[Any]
total: int
per_page: int
items_attribute: str = "results"
```
## Apply pagination to multiple operations at once
There is often a case when you need to add pagination to all views that returns querysets or list
You can use a builtin router class (`RouterPaginated`) that automatically injects pagination to all operations that defined `response=List[SomeSchema]`:
```python hl_lines="1 3 6 10"
from ninja.pagination import RouterPaginated
router = RouterPaginated()
@router.get("/items", response=List[MySchema])
def items(request):
return MyModel.objects.all()
@router.get("/other-items", response=List[OtherSchema])
def other_items(request):
return OtherModel.objects.all()
```
In this example both operations will have pagination enabled
to apply pagination to main `api` instance use `default_router` argument:
```python
api = NinjaAPI(default_router=RouterPaginated())
@api.get(...
```
vitalik-django-ninja-0b67d47/docs/docs/guides/response/response-renderers.md 0000664 0000000 0000000 00000006255 15156602544 0027236 0 ustar 00root root 0000000 0000000 # Response renderers
The most common response type for a REST API is usually JSON.
**Django Ninja** also has support for defining your own custom renderers, which gives you the flexibility to design your own media types.
## Create a renderer
To create your own renderer, you need to inherit `ninja.renderers.BaseRenderer` and override the `render` method. Then you can pass an instance of your class to `NinjaAPI` as the `renderer` argument:
```python hl_lines="5 8 9"
from ninja import NinjaAPI
from ninja.renderers import BaseRenderer
class MyRenderer(BaseRenderer):
media_type = "text/plain"
def render(self, request, data, *, response_status):
return ... # your serialization here
api = NinjaAPI(renderer=MyRenderer())
```
The `render` method takes the following arguments:
- request -> HttpRequest object
- data -> object that needs to be serialized
- response_status as an `int` -> the HTTP status code that will be returned to the client
You need also define the `media_type` attribute on the class to set the content-type header for the response.
## ORJSON renderer example:
[orjson](https://github.com/ijl/orjson#orjson) is a fast, accurate JSON library for Python. It benchmarks as the fastest Python library for JSON and is more accurate than the standard `json` library or other third-party libraries. It also serializes dataclass, datetime, numpy, and UUID instances natively.
Here's an example renderer class that uses `orjson`:
```python hl_lines="9 10"
import orjson
from ninja import NinjaAPI
from ninja.renderers import BaseRenderer
class ORJSONRenderer(BaseRenderer):
media_type = "application/json"
def render(self, request, data, *, response_status):
return orjson.dumps(data)
api = NinjaAPI(renderer=ORJSONRenderer())
```
## XML renderer example:
This is how you create a renderer that outputs all responses as XML:
```python hl_lines="8 11"
from io import StringIO
from django.utils.encoding import force_str
from django.utils.xmlutils import SimplerXMLGenerator
from ninja import NinjaAPI
from ninja.renderers import BaseRenderer
class XMLRenderer(BaseRenderer):
media_type = "text/xml"
def render(self, request, data, *, response_status):
stream = StringIO()
xml = SimplerXMLGenerator(stream, "utf-8")
xml.startDocument()
xml.startElement("data", {})
self._to_xml(xml, data)
xml.endElement("data")
xml.endDocument()
return stream.getvalue()
def _to_xml(self, xml, data):
if isinstance(data, (list, tuple)):
for item in data:
xml.startElement("item", {})
self._to_xml(xml, item)
xml.endElement("item")
elif isinstance(data, dict):
for key, value in data.items():
xml.startElement(key, {})
self._to_xml(xml, value)
xml.endElement(key)
elif data is None:
# Don't output any value
pass
else:
xml.characters(force_str(data))
api = NinjaAPI(renderer=XMLRenderer())
```
*(Copyright note: this code is basically copied from [DRF-xml](https://jpadilla.github.io/django-rest-framework-xml/))*
vitalik-django-ninja-0b67d47/docs/docs/guides/response/temporal_response.md 0000664 0000000 0000000 00000002634 15156602544 0027147 0 ustar 00root root 0000000 0000000 # Altering the Response
Sometimes you'll want to change the response just before it gets served, for example, to add a header or alter a cookie.
To do this, simply declare a function parameter with a type of `HttpResponse`:
```python
from django.http import HttpRequest, HttpResponse
@api.get("/cookie/")
def feed_cookiemonster(request: HttpRequest, response: HttpResponse):
# Set a cookie.
response.set_cookie("cookie", "delicious")
# Set a header.
response["X-Cookiemonster"] = "blue"
return {"cookiemonster_happy": True}
```
## Temporal response object
This response object is used for the base of all responses built by Django Ninja, including error responses. This object is *not* used if a Django `HttpResponse` object is returned directly by an operation.
Obviously this response object won't contain the content yet, but it does have the `content_type` set (but you probably don't want to be changing it).
The `status_code` will get overridden depending on the return value (200 by default, or the status code if a two-part tuple is returned).
## Changing the base response object
You can alter this temporal response object by overriding the `NinjaAPI.create_temporal_response` method.
```python
def create_temporal_response(self, request: HttpRequest) -> HttpResponse:
response = super().create_temporal_response(request)
# Do your magic here...
return response
``` vitalik-django-ninja-0b67d47/docs/docs/guides/routers.md 0000664 0000000 0000000 00000012727 15156602544 0023257 0 ustar 00root root 0000000 0000000 # Routers
Real world applications can almost never fit all logic into a single file.
**Django Ninja** comes with an easy way to split your API into multiple modules using Routers.
Let's say you have a Django project with a structure like this:
```
├── myproject
│ └── settings.py
├── events/
│ ├── __init__.py
│ └── models.py
├── news/
│ ├── __init__.py
│ └── models.py
├── blogs/
│ ├── __init__.py
│ └── models.py
└── manage.py
```
To add API's to each of the Django applications, create an `api.py` module in each app:
``` hl_lines="5 9 13"
├── myproject
│ └── settings.py
├── events/
│ ├── __init__.py
│ ├── api.py
│ └── models.py
├── news/
│ ├── __init__.py
│ ├── api.py
│ └── models.py
├── blogs/
│ ├── __init__.py
│ ├── api.py
│ └── models.py
└── manage.py
```
Now let's add a few operations to `events/api.py`. The trick is that instead of using the `NinjaAPI` class, you use the **Router** class:
```python hl_lines="1 4 6 13"
from ninja import Router
from .models import Event
router = Router()
@router.get('/')
def list_events(request):
return [
{"id": e.id, "title": e.title}
for e in Event.objects.all()
]
@router.get('/{event_id}')
def event_details(request, event_id: int):
event = Event.objects.get(id=event_id)
return {"title": event.title, "details": event.details}
```
Then do the same for the `news` app with `news/api.py`:
```python hl_lines="1 4"
from ninja import Router
from .models import News
router = Router()
@router.get('/')
def list_news(request):
...
@router.get('/{news_id}')
def news_details(request, news_id: int):
...
```
and then also `blogs/api.py`.
Finally, let's group them together.
In your top level project folder (next to `urls.py`), create another `api.py` file with the main `NinjaAPI` instance:
``` hl_lines="2"
├── myproject
│ ├── api.py
│ └── settings.py
├── events/
│ ...
├── news/
│ ...
├── blogs/
│ ...
```
It should look like this:
```python
from ninja import NinjaAPI
api = NinjaAPI()
```
Now we import all the routers from the various apps, and include them into the main API instance:
```python hl_lines="2 6 7 8"
from ninja import NinjaAPI
from events.api import router as events_router
api = NinjaAPI()
api.add_router("/events/", events_router) # You can add a router as an object
api.add_router("/news/", "news.api.router") # or by Python path
api.add_router("/blogs/", "blogs.api.router")
```
Now, include `api` to your urls as usual and open your browser at `/api/docs`, and you should see all your routers combined into a single API:

## Router authentication
Use `auth` argument to apply authenticator to all operations declared by router:
```python
api.add_router("/events/", events_router, auth=BasicAuth())
```
or using router constructor
```python
router = Router(auth=BasicAuth())
```
## Router tags
You can use `tags` argument to apply tags to all operations declared by router:
```python
api.add_router("/events/", events_router, tags=["events"])
```
or using router constructor
```python
router = Router(tags=["events"])
```
## Nested routers
There are also times when you need to split your logic up even more.
**Django Ninja** makes it possible to include a router into another router as many times as you like, and finally include the top level router into the main `api` instance.
Basically, what that means is that you have `add_router` both on the `api` instance and on the `router` instance:
```python hl_lines="7 8 9 32 33 34"
from django.contrib import admin
from django.urls import path
from ninja import NinjaAPI, Router
api = NinjaAPI()
first_router = Router()
second_router = Router()
third_router = Router()
@api.get("/add")
def add(request, a: int, b: int):
return {"result": a + b}
@first_router.get("/add")
def add(request, a: int, b: int):
return {"result": a + b}
@second_router.get("/add")
def add(request, a: int, b: int):
return {"result": a + b}
@third_router.get("/add")
def add(request, a: int, b: int):
return {"result": a + b}
second_router.add_router("l3", third_router)
first_router.add_router("l2", second_router)
api.add_router("l1", first_router)
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", api.urls),
]
```
Now you have the following endpoints:
```
/api/add
/api/l1/add
/api/l1/l2/add
/api/l1/l2/l3/add
```
Great! Now go have a look at the automatically generated docs:

### Nested url parameters
You can also use url parameters in nested routers by adding `= Path(...)` to the function parameters:
```python hl_lines="13 16"
from django.contrib import admin
from django.urls import path
from ninja import NinjaAPI, Path, Router
api = NinjaAPI()
router = Router()
@api.get("/add/{a}/{b}")
def add(request, a: int, b: int):
return {"result": a + b}
@router.get("/multiply/{c}")
def multiply(request, c: int, a: int = Path(...), b: int = Path(...)):
return {"result": (a + b) * c}
api.add_router("add/{a}/{b}", router)
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", api.urls),
]
```
This will generate the following endpoints:
```
/api/add/{a}/{b}
/api/add/{a}/{b}/multiply/{c}
``` vitalik-django-ninja-0b67d47/docs/docs/guides/testing.md 0000664 0000000 0000000 00000005143 15156602544 0023223 0 ustar 00root root 0000000 0000000 # Testing
**Django Ninja** is fully compatible with standard [django test client](https://docs.djangoproject.com/en/dev/topics/testing/tools/) , but also provides a test client to make it easy to test just APIs without middleware/url-resolver layer making tests run faster.
To test the following API:
```python
from ninja import NinjaAPI, Schema
api = NinjaAPI()
router = Router()
class HelloResponse(Schema):
msg: str
@router.get("/hello", response=HelloResponse)
def hello(request):
return {"msg": "Hello World"}
api.add_router("", router)
```
You can use the Django test class:
```python
from django.test import TestCase
from ninja.testing import TestClient
class HelloTest(TestCase):
def test_hello(self):
# don't forget to import router from code above
client = TestClient(router)
response = client.get("/hello")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"msg": "Hello World"})
```
It is also possible to access the deserialized data using the `data` property:
```python
self.assertEqual(response.data, {"msg": "Hello World"})
```
## Attributes
Arbitrary attributes can be added to the request object by passing keyword arguments to the client request methods:
```python
class HelloTest(TestCase):
def test_hello(self):
client = TestClient(router)
# request.company_id will now be set within the view
response = client.get("/hello", company_id=1)
```
### Headers
It is also possible to specify headers, both from the TestCase instantiation and the actual request:
```python
client = TestClient(router, headers={"A": "a", "B": "b"})
# The request will be made with {"A": "na", "B": "b", "C": "nc"} headers
response = client.get("/test-headers", headers={"A": "na", "C": "nc"})
```
### Cookies
It is also possible to specify cookies, both from the TestCase instantiation and the actual request:
```python
client = TestClient(router, COOKIES={"A": "a", "B": "b"})
# The request will be made with {"A": "na", "B": "b", "C": "nc"} cookies
response = client.get("/test-cookies", COOKIES={"A": "na", "C": "nc"})
```
### Users
It is also possible to specify a User for the request:
```python
user = User.objects.create(...)
client = TestClient(router)
# The request will be made with user logged in
response = client.get("/test-with-user", user=user)
```
## Testing async operations
To test operations in async context use `TestAsyncClient`:
```python
from ninja.testing import TestAsyncClient
client = TestAsyncClient(router)
response = await client.post("/test/")
```
vitalik-django-ninja-0b67d47/docs/docs/guides/throttling.md 0000664 0000000 0000000 00000010123 15156602544 0023736 0 ustar 00root root 0000000 0000000 # Throttling
Throttles allows to control the rate of requests that clients can make to an API. Django Ninja allows to set custom throttlers globally (across all operations in NinjaAPI instance), on router level and each operation individually.
!!! note
The application-level throttling that Django Ninja provides should not be considered a security measure or protection against brute forcing or denial-of-service attacks. Deliberately malicious actors will always be able to spoof IP origins. The built-in throttling implementations are implemented using Django's cache framework, and use non-atomic operations to determine the request rate, which may sometimes result in some fuzziness.
Django Ninja’s throttling feature is pretty much based on what Django Rest Framework (DRF) uses, which you can check out [here](https://www.django-rest-framework.org/api-guide/throttling/). So, if you’ve already got custom throttling set up for DRF, there’s a good chance it’ll work with Django Ninja right out of the box. The key difference is that you need to pass initialized Throttle objects instead of classes (which should give a better performance).
You can specify a rate using the format requests/time-unit, where time-unit represents a number of units followed by an optional unit of time. If the unit is omitted, it defaults to seconds. For example, the following are equivalent and all represent "100 requests per 5 minutes":
* 100/5m
* 100/300s
* 100/300
The following units are supported:
* `s` or `sec`
* `m` or `min`
* `h` or `hour`
* `d` or `day`
## Usage
### Global
The following example will limit unauthenticated users to only 10 requests per second, while authenticated can make 100/s
```Python
from ninja.throttling import AnonRateThrottle, AuthRateThrottle
api = NinjaAPI(
throttle=[
AnonRateThrottle('10/s'),
AuthRateThrottle('100/s'),
],
)
```
!!! tip
`throttle` argument accepts single object and list of throttle objects
### Router level
Pass `throttle` argument either to `add_router` function
```Python
api = NinjaAPI()
...
api.add_router('/sensitive', 'myapp.api.router', throttle=AnonRateThrottle('100/m'))
```
or directly to init of the Router class:
```Python
router = Router(..., throttle=[AnonRateThrottle('1000/h')])
```
### Operation level
If `throttle` argument is passed to operation - it will overrule all global and router throttles:
```Python
from ninja.throttling import UserRateThrottle
@api.get('/some', throttle=[UserRateThrottle('10000/d')])
def some(request):
...
```
## Builtin throttlers
### AnonRateThrottle
Will only throttle unauthenticated users. The IP address of the incoming request is used to generate a unique key to throttle against.
### UserRateThrottle
Will throttle users (**if you use django build-in user authentication**) to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticated requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against.
### AuthRateThrottle
Will throttle by Django ninja [authentication](authentication.md) to a given rate of requests across the API. Unauthenticated requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against.
Note: the cache key in case of `request.auth` will be generated by `sha256(str(request.auth))` - so if you returning some custom objects inside authentication make sure to implement `__str__` method that will return a unique value for the user.
## Custom throttles
To create a custom throttle, override `BaseThrottle` (or any of builtin throttles) and implement `.allow_request(self, request)`. The method should return `True` if the request should be allowed, and `False` otherwise.
Example
```Python
from ninja.throttling import AnonRateThrottle
class NoReadsThrottle(AnonRateThrottle):
"""Do not throttle GET requests"""
def allow_request(self, request):
if request.method == "GET":
return True
return super().allow_request(request)
```
vitalik-django-ninja-0b67d47/docs/docs/guides/urls.md 0000664 0000000 0000000 00000003341 15156602544 0022531 0 ustar 00root root 0000000 0000000 # Reverse Resolution of URLS
A reverse URL name is generated for each method in a Django Ninja Schema (or `Router`).
## How URLs are generated
The URLs are all contained within a namespace, which defaults to `"api-1.0.0"`, and each URL name matches the function it is decorated.
For example:
```python
api = NinjaAPI()
@api.get("/")
def index(request):
...
index_url = reverse_lazy("api-1.0.0:index")
```
This implicit URL name will only be set for the first operation for each API path. If you *don't* want any implicit reverse URL name generated, just explicitly specify `url_name=""` (an empty string) on the method decorator.
### Changing the URL name
Rather than using the default URL name, you can specify it explicitly as a property on the method decorator.
```python
@api.get("/users", url_name="user_list")
def users(request):
...
users_url = reverse_lazy("api-1.0.0:user_list")
```
This will override any implicit URL name to this API path.
#### Overriding default url names
You can also override implicit url naming by overwriting the `get_operation_url_name` method:
```python
class MyAPI(NinjaAPI):
def get_operation_url_name(self, operation, router):
return operation.view_func.__name__ + '_my_extra_suffix'
api = MyAPI()
```
### Customizing the namespace
The default URL namespace is built by prepending the Schema's version with `"api-"`, however you can explicitly specify the namespace by overriding the `urls_namespace` attribute of the `NinjaAPI` Schema class.
```python
api = NinjaAPI(auth=token_auth, version='2')
api_private = NinjaAPI(auth=session_auth, urls_namespace='private_api')
api_users_url = reverse_lazy("api-2:users")
private_api_admins_url = reverse_lazy("private_api:admins")
```
vitalik-django-ninja-0b67d47/docs/docs/guides/versioning.md 0000664 0000000 0000000 00000002732 15156602544 0023732 0 ustar 00root root 0000000 0000000 # Versioning
## Different API version numbers
With **Django Ninja** it's easy to run multiple API versions from a single Django project.
All you have to do is create two or more NinjaAPI instances with different `version` arguments:
**api_v1.py**:
```python hl_lines="4"
from ninja import NinjaAPI
api = NinjaAPI(version='1.0.0')
@api.get('/hello')
def hello(request):
return {'message': 'Hello from V1'}
```
api_**v2**.py:
```python hl_lines="4"
from ninja import NinjaAPI
api = NinjaAPI(version='2.0.0')
@api.get('/hello')
def hello(request):
return {'message': 'Hello from V2'}
```
and then in **urls.py**:
```python hl_lines="8 9"
...
from api_v1 import api as api_v1
from api_v2 import api as api_v2
urlpatterns = [
...
path('api/v1/', api_v1.urls),
path('api/v2/', api_v2.urls),
]
```
Now you can go to different OpenAPI docs pages for each version:
- http://127.0.0.1/api/**v1**/docs
- http://127.0.0.1/api/**v2**/docs
## Different business logic
In the same way, you can define a different API for different components or areas:
```python hl_lines="4 7"
...
api = NinjaAPI(auth=token_auth, urls_namespace='public_api')
...
api_private = NinjaAPI(auth=session_auth, urls_namespace='private_api')
...
urlpatterns = [
...
path('api/', api.urls),
path('internal-api/', api_private.urls),
]
```
!!! note
If you use different **NinjaAPI** instances, you need to define different `version`s or different `urls_namespace`s.
vitalik-django-ninja-0b67d47/docs/docs/help.md 0000664 0000000 0000000 00000002717 15156602544 0021222 0 ustar 00root root 0000000 0000000 # Help / Get Help
## Do you like Django Ninja?
If you like this project, there is a tiny thing you can do to let us know that we're moving in the right direction.
Simply give django-ninja a star on github
or share this URL on social media:
```
https://django-ninja.dev
```
Follow updates on twitter @django_ninja
## Do you want to help us?
Pull requests are always welcome.
You can inspect our docs for typos and spelling mistakes, and create pull requests or open an issue.
If you have any suggestions to improve **Django Ninja**, please create them as issues on GitHub.
## Do you need help?
Do not hesitate. Go to GitHub issues and describe your question or problem. We'll attempt to address them quickly.
Join the chat at our Discord server.
[Code-on the webdesign and web development company](https://code-on.be/) gives commercial consulting for Django-Ninja. If you are looking for support please contact Code-on and we will be in touch with you soon.
vitalik-django-ninja-0b67d47/docs/docs/img/ 0000775 0000000 0000000 00000000000 15156602544 0020515 5 ustar 00root root 0000000 0000000 vitalik-django-ninja-0b67d47/docs/docs/img/auth-swagger-ui-prompt.png 0000664 0000000 0000000 00000066772 15156602544 0025575 0 ustar 00root root 0000000 0000000 PNG
IHDR BS FiCCPICC Profile (c``I,(aa``+)
rwRR`
>@%0|/]6vUܞ_B0գ d ӒJSl):
ȞbC@$XMH3}HHIBOGbCWP#C%VhʢG`(*x%(00s8,!00X|c``KABLeöCEp0~c)N36yXY}"߉^@o30 p_CrO VeXIfMM * i D ASCII Screenshot+R5 iTXtXML:com.adobe.xmp 702Screenshot394
<ک @ IDATxx\Ն%KbcM@hB JH BKP$ ;ƽE%[?gVs}wujwZó{Nw۳gL6C:CAA&tM|BR @ P6i @ 9G sK @ 2A @ s9$ @ J @ 9G sK @ 2A @ s9$ @ J @ 9G sK @ 2A @ s9$ @ J @ 9G sK @ 2A @ s9$ @ J @ 9G sK @ 2A @ s9$ @ J @ 9G sK @ 2A8&fkk{U'%}o5kH{{ASm.zM2xxD @ CDžoKYl7۲>u2oPĈAIYmjje^O4·ޫWq(ҸB @L# ܺkii%KWx @ K /}|Z=Je}eؑ2tH{Q @ E`'l:ZWM!·C[k[[[RڥɺX ~
ǟlZVSo
ո]u2.O@ F!qT6#:ZWM!+
fq?XbU+(
KYv,YR%msE #f9sKsKx>W~}eQ5RorMyRkV[VEUV60V-U4ϞлWhA4ybf͞/EEl#@ L(2L"}|2ydoNE-))u2nUѻ`.W!m0yKRk
ѫV\mk{"GPzZq=ayW[ejVmF2 vF! @ *dUƲvѫθ𭭫 kzҪe]bEpn3[J824ҳxS-Z'UjIMaap*X{t-@ K WfaŊF*yʸ 2/s
^ב#GH0w|ii
WC0oRzW_J>lw_k\(3nc=Wx}vcyęScCcmCs#@ lp7!_D̨W}YܪuAVh*Cus}^
%%ƊajoXw^d|2~(eB[K =5̨W-PQa_ex5[yL
z5/M
jqƏK[AH @ N_g֍lAkaګuCqq"
%z>I9"s@ `ůѓ4-BL
1h5O*--.
.
uy#qY:Hl(4V2S_]<ȵ6A @ B@[n/Q[jÑϫuJu~4 ם/2!/B$
; vyk#\8b=P
@ г2*|'Dsn
UCuм>*|ׅeVSPu'4D>Tb]uH
!j]][p'i#*bbez>kkOiv @
_#%˽!̝[jE
hAeUZfEj#Ej<1ye}%W=
':D1g771"o|uRWU-bN(G@ }K9g]g:ѫᠠpaYzn*cT?Ɯ)"*vޔu*]Ч-ZLM,eؐuǧiEw'Du岲֞:ѝv@ Е@Z.'ƀ~}e1naLil֟6>cnj6(T<6H]Їc2m:7%KǦՅba^Q" @ #P0m4D$[C)
jIM4]&˂ G&ږF7=ϷT'!kŸFwÜ\TxKmPA #wuiHzE"AXHѸeTT8cOJ91y)7HE@ 0wu@ o[@ @f7A o[@ @f7A o[@ @f7A o[@ @f7A o[@ @f7A o[@ @f7A o[@ (NTQ @ )HVLIVTts*C @ [pui @ QO @ "#[c@ @:\lS*zuW]|=_@ "ȍO餅-kEj[imtM@ D'(wȀ>\5p9I _j,A2PtI @ H@Ѻ
k
diChMJUֶ[&ү4yP @ 1 q-2[ߔNuhZ[ }KbL@ @ѝ?RӋ{CwS @ ;ӱ,i
,e @ M:4ixn2Nȭ"L]ͩh.122@ Ni*zZ9<b
)ʴ@ @.H!K]X^ ۳
V1kTQ ~ЧIڵkQ֬YcSKJJU^z~4wG
UPK |";V]V ]kk¢\1˕ @ 456ڬ>}Hy
ojlѼʊJ}p=VgBJj@ dsoP[h V?b0_EbF{ @ @vokksI&ߌ jUMoشO)/5ؖʽemx/BBqO)Aќbm
M-وU<@ k<$:UDWpY|"*E$o(/xyדJ}C_ph!B[K"^B4Cubw3XAƦ&пw麵ҺoKV}rz ̓՜˦ ΠuӉ!VW~fFCkCShMÍ ^ZdS0do]ukLVc)֠_VJBeך3n2
SPVlO
ptɲeeV;v\TL+V4;F[]eDw}]V1O>l76Ϡ61|?̗b W6CGśʕ+l5TVv
(ʆ uÏ3(ٙNVKJ57YڬhYkY\B/T?(TEC48h|zU/6?V# W˨Qm%Fl74khYB;