Skip to content

Commit 785d9c2

Browse files
committed
Initial commit
0 parents  commit 785d9c2

25 files changed

+2619
-0
lines changed

.github/workflows/publish.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: PyPI Release
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
8+
jobs:
9+
10+
publish-release:
11+
name: upload release to PyPI
12+
runs-on: ubuntu-latest
13+
permissions:
14+
id-token: write
15+
environment: release
16+
steps:
17+
- name: Check out repository
18+
uses: actions/checkout@v4
19+
20+
- uses: actions/setup-python@v5
21+
with:
22+
python-version: "3.12"
23+
24+
- name: Install uv
25+
uses: astral-sh/setup-uv@v6
26+
with:
27+
enable-cache: true
28+
29+
- name: Build package
30+
run: uv build
31+
32+
- name: Publish package distributions to PyPI
33+
uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/test.yaml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Tests And Linting
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
validate:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-python@v5
16+
with:
17+
python-version: "3.13"
18+
19+
- name: Install Pre-Commit
20+
run: python -m pip install pre-commit && pre-commit install
21+
22+
- name: Load cached Pre-Commit Dependencies
23+
id: cached-pre-commit-dependencies
24+
uses: actions/cache@v4
25+
with:
26+
path: ~/.cache/pre-commit/
27+
key: pre-commit|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
28+
29+
- name: Execute Pre-Commit
30+
run: pre-commit run --show-diff-on-failure --color=always --all-files
31+
32+
mypy:
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@v4
36+
37+
- uses: actions/setup-python@v5
38+
with:
39+
python-version: "3.9"
40+
allow-prereleases: true
41+
42+
- name: Install uv
43+
uses: astral-sh/setup-uv@v6
44+
with:
45+
enable-cache: true
46+
47+
- name: Run mypy
48+
run: uv run mypy
49+
50+
test:
51+
runs-on: ubuntu-latest
52+
name: Test (Py ${{ matrix.python-version }}, Dj ${{ matrix.django-version }})
53+
strategy:
54+
fail-fast: true
55+
matrix:
56+
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
57+
django-version: [ "4.1", "5.1" ]
58+
exclude:
59+
- python-version: 3.9
60+
django-version: 5.1
61+
62+
steps:
63+
- name: Check out repository
64+
uses: actions/checkout@v4
65+
66+
- name: Set up python ${{ matrix.python-version }}
67+
uses: actions/setup-python@v5
68+
with:
69+
python-version: ${{ matrix.python-version }}
70+
71+
- name: Install uv
72+
uses: astral-sh/setup-uv@v6
73+
with:
74+
enable-cache: true
75+
76+
- name: Test
77+
run: >
78+
uv run
79+
--python=${{ matrix.python-version }}
80+
--with="django~=${{ matrix.django-version }}"
81+
python -m pytest

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
.idea
9+
10+
# Virtual environments
11+
.venv

.idea/.gitignore

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.pre-commit-config.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
default_language_version:
2+
python: "3.13"
3+
repos:
4+
- repo: https://github.com/compilerla/conventional-pre-commit
5+
rev: v4.0.0
6+
hooks:
7+
- id: conventional-pre-commit
8+
stages: [commit-msg]
9+
- repo: https://github.com/pre-commit/pre-commit-hooks
10+
rev: v5.0.0
11+
hooks:
12+
- id: check-ast
13+
- id: check-case-conflict
14+
- id: check-merge-conflict
15+
- id: check-toml
16+
- id: debug-statements
17+
- id: end-of-file-fixer
18+
- id: mixed-line-ending
19+
- id: trailing-whitespace
20+
- repo: https://github.com/astral-sh/ruff-pre-commit
21+
rev: "v0.11.0"
22+
hooks:
23+
- id: ruff
24+
args: ["--fix"]
25+
- id: ruff-format
26+
- repo: https://github.com/crate-ci/typos
27+
rev: v1.30.3
28+
hooks:
29+
- id: typos

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Litestar
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Litestar-Django
2+
3+
Django model support for Litestar, implemented via Litestar [DTOs](https://docs.litestar.dev/latest/usage/dto/index.html).
4+
5+
```python
6+
from litestar import get, Litestar
7+
from litestar_django import DjangoModelPlugin
8+
from django.db import models
9+
10+
class Author(models.Model):
11+
name = models.CharField(max_length=100)
12+
13+
14+
class Genre(models.Model):
15+
name = models.CharField(max_length=50)
16+
17+
18+
class Book(models.Model):
19+
name = models.CharField()
20+
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
21+
genres = models.ManyToManyField(Genre, related_name="books")
22+
23+
24+
@get("/{author_id:int}")
25+
async def handler(author_id: int) -> Author:
26+
return await Author.objects.prefetch_related("books").aget(id=author_id)
27+
28+
29+
app = Litestar([handler], plugins=[DjangoModelPlugin()])
30+
```
31+
32+
This minimal setup will provide serialization of Django objects returned from handlers,
33+
complete with OpenAPI schema generation.
34+
35+
## Installation
36+
37+
```bash
38+
pip install litestar-django
39+
```
40+
41+
## Usage
42+
43+
### Directly constructing a DTO
44+
45+
```python
46+
from litestar import get
47+
from litestar_django import DjangoModelDTO
48+
from app.models import Author
49+
50+
@get("/{author_id:int}", dto=DjangoModelDTO[Author])
51+
async def handler(author_id: int) -> Author:
52+
return await Author.objects.prefetch_related("books").aget(id=author_id)
53+
```
54+
55+
### Automatically creating DTOs via the plugin
56+
57+
```python
58+
from litestar import get
59+
from litestar_django import DjangoModelPlugin
60+
from app.models import Author
61+
62+
@get("/{author_id:int}")
63+
async def handler(author_id: int) -> Author:
64+
return await Author.objects.prefetch_related("books").aget(id=author_id)
65+
66+
app = Litestar([handler], plugins=[DjangoModelPlugin()])
67+
```
68+
69+
### Creating a model instance from a DTO
70+
71+
```python
72+
from typing import Annotated
73+
from litestar import post
74+
from litestar.dto import DTOConfig
75+
from litestar_django import DjangoModelDTO
76+
from app.models import Author
77+
78+
@post(
79+
"/",
80+
sync_to_thread=True,
81+
dto=DjangoModelDTO[
82+
Annotated[
83+
Author,
84+
# exclude primary key and relationship fields
85+
DTOConfig(exclude={"id", "books"})
86+
]
87+
],
88+
return_dto=DjangoModelDTO[Author],
89+
)
90+
async def handler(data: Author) -> Author:
91+
await data.asave()
92+
return data
93+
```
94+
95+
## OpenAPI
96+
97+
Full OpenAPI schemas are generated from models based on their field types:
98+
99+
### Type map
100+
101+
| Field | OpenAPI type | OpenAPI format |
102+
|------------------------|--------------|----------------|
103+
| `models.JSONField` | `{}` | |
104+
| `models.DecimalField` | `number` | |
105+
| `models.DateTimeField` | `string` | `date-time` |
106+
| `models.DateField` | `string` | `date` |
107+
| `models.TimeField` | `string` | `duration` |
108+
| `models.DurationField` | `string` | `duration` |
109+
| `models.FileField` | `string` | |
110+
| `models.FilePathField` | `string` | |
111+
| `models.UUIDField` | `string` | `uuid` |
112+
| `models.IntegerField` | `integer` | |
113+
| `models.FloatField` | `number` | |
114+
| `models.BooleanField` | `boolean` | |
115+
| `models.CharField` | `string` | |
116+
| `models.TextField` | `string` | |
117+
| `models.BinaryField` | `string` | `byte` |
118+
119+
### Additional properties
120+
121+
The following properties are extracted from fields, in addition to its type:
122+
123+
124+
| OpenAPI property | From |
125+
|--------------------|----------------------|
126+
| `title` | `Field.verbose_name` |
127+
| `description` | `Field.help_text` |
128+
| `enum` | `Field.choices` |
129+
| `exclusiveMinimum` | `MinValueValidator` |
130+
| `exclusiveMaximum` | `MaxValueValidator` |
131+
| `minLength` | `MinLengthValidator` |
132+
| `maxLength` | `MaxLengthValidator` |
133+
134+
### Relationships
135+
136+
Relationships will be represented as individual components, referenced in the schema.
137+
138+
139+
## Lazy loading
140+
141+
> [!IMPORTANT]
142+
> Since lazy-loading is not supported in an async context, you must ensure to always
143+
> load everything consumed by the DTO. Not doing so will result in a
144+
> [`SynchronousOnlyOperation`](https://docs.djangoproject.com/en/5.2/ref/exceptions/#django.core.exceptions.SynchronousOnlyOperation)
145+
> exception being raised by Django
146+
147+
This can be mitigated by:
148+
149+
1. Setting `include` or `exclude` rules to only include necessary fields ([docs](https://docs.litestar.dev/latest/usage/dto/1-abstract-dto.html#excluding-fields))
150+
2. Configuring nested relationships with an appropriate `max_nexted_depth`
151+
([docs](https://docs.litestar.dev/latest/usage/dto/1-abstract-dto.html#nested-fields))
152+
3. Using [`select_related`](https://docs.djangoproject.com/en/5.2/ref/models/querysets/#select-related)
153+
and [`prefetch_related`](https://docs.djangoproject.com/en/5.2/ref/models/querysets/#prefetch-related)
154+
to ensure relationships are fully loaded
155+
156+
157+
158+
## Contributing
159+
160+
All [Litestar Organization][litestar-org] projects are open for contributions of any
161+
size and form.
162+
163+
If you have any questions, reach out to us on [Discord][discord] or our org-wide
164+
[GitHub discussions][litestar-discussions] page.
165+
166+
<!-- markdownlint-disable -->
167+
<hr />
168+
<p align="center">
169+
<!-- github-banner-start -->
170+
<img src="https://raw.githubusercontent.com/litestar-org/branding/main/assets/Branding%20-%20SVG%20-%20Transparent/Organization%20Project%20-%20Banner%20-%20Inline%20-%20Dark.svg" alt="Litestar Logo - Light" width="40%" height="auto" />
171+
<br>An official <a href="https://github.com/litestar-org">Litestar Organization</a> Project
172+
<!-- github-banner-end -->
173+
</p>
174+
175+
[litestar-org]: https://github.com/litestar-org
176+
[discord]: https://discord.gg/litestar
177+
[litestar-discussions]: https://github.com/orgs/litestar-org/discussions

litestar_django/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from litestar_django.plugin import DjangoModelPlugin
2+
from litestar_django.dto import DjangoModelDTO, DjangoDTOConfig
3+
4+
__all__ = (
5+
"DjangoModelDTO",
6+
"DjangoDTOConfig",
7+
"DjangoModelPlugin",
8+
)

0 commit comments

Comments
 (0)