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