Skip to content

Commit 7d583ad

Browse files
authored
Merge pull request #2 from litestar-org/ci
2 parents 94b630c + 2a34496 commit 7d583ad

15 files changed

+387
-85
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

.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

README.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Book(models.Model):
1919
name = models.CharField()
2020
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
2121
genres = models.ManyToManyField(Genre, related_name="books")
22-
22+
2323

2424
@get("/{author_id:int}")
2525
async def handler(author_id: int) -> Author:
@@ -80,11 +80,11 @@ from app.models import Author
8080
sync_to_thread=True,
8181
dto=DjangoModelDTO[
8282
Annotated[
83-
Author,
83+
Author,
8484
# exclude primary key and relationship fields
8585
DTOConfig(exclude={"id", "books"})
86-
]
87-
],
86+
]
87+
],
8888
return_dto=DjangoModelDTO[Author],
8989
)
9090
async def handler(data: Author) -> Author:
@@ -138,29 +138,29 @@ Relationships will be represented as individual components, referenced in the sc
138138

139139
## Lazy loading
140140

141-
> [!IMPORTANT]
141+
> [!IMPORTANT]
142142
> 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
143+
> load everything consumed by the DTO. Not doing so will result in a
144144
> [`SynchronousOnlyOperation`](https://docs.djangoproject.com/en/5.2/ref/exceptions/#django.core.exceptions.SynchronousOnlyOperation)
145145
> exception being raised by Django
146146
147147
This can be mitigated by:
148148

149149
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`
150+
2. Configuring nested relationships with an appropriate `max_nexted_depth`
151151
([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)
152+
3. Using [`select_related`](https://docs.djangoproject.com/en/5.2/ref/models/querysets/#select-related)
153153
and [`prefetch_related`](https://docs.djangoproject.com/en/5.2/ref/models/querysets/#prefetch-related)
154154
to ensure relationships are fully loaded
155155

156156

157157

158158
## Contributing
159159

160-
All [Litestar Organization][litestar-org] projects are open for contributions of any
160+
All [Litestar Organization][litestar-org] projects are open for contributions of any
161161
size and form.
162162

163-
If you have any questions, reach out to us on [Discord][discord] or our org-wide
163+
If you have any questions, reach out to us on [Discord][discord] or our org-wide
164164
[GitHub discussions][litestar-discussions] page.
165165

166166
<!-- markdownlint-disable -->
@@ -175,5 +175,3 @@ If you have any questions, reach out to us on [Discord][discord] or our org-wide
175175
[litestar-org]: https://github.com/litestar-org
176176
[discord]: https://discord.gg/litestar
177177
[litestar-discussions]: https://github.com/orgs/litestar-org/discussions
178-
179-

litestar_django/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from litestar_django.dto import DjangoModelDTO, DjangoDTOConfig
33

44
__all__ = (
5-
DjangoModelDTO,
6-
DjangoDTOConfig,
7-
DjangoModelPlugin,
5+
"DjangoModelDTO",
6+
"DjangoDTOConfig",
7+
"DjangoModelPlugin",
88
)

litestar_django/dto.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22
import datetime
33
import decimal
44
import uuid
5-
from typing import TypeVar, Generic, Type, Any, Generator, List, Optional, Callable
5+
from typing import (
6+
TypeVar,
7+
Generic,
8+
Type,
9+
Any,
10+
Generator,
11+
List,
12+
Optional,
13+
Callable,
14+
Mapping,
15+
)
616

717
from django.core import validators # type: ignore[import-untyped]
818
from django.db import models # type: ignore[import-untyped]
@@ -50,7 +60,7 @@
5060
}
5161

5262
try:
53-
from enumfields import EnumField
63+
from enumfields import EnumField # type: ignore[import-untyped]
5464
except ImportError:
5565
EnumField = None
5666

@@ -73,7 +83,7 @@ class DjangoDTOConfig(DTOConfig):
7383

7484
class DjangoModelDTO(AbstractDTO[T], Generic[T]):
7585
attribute_accessor = _get_model_attribute
76-
custom_field_types: dict[type[AnyField], Any] | None = None
86+
custom_field_types: Optional[dict[type[AnyField], Any]] = None
7787

7888
@classmethod
7989
def get_field_type(cls, field: Field, type_map: dict[type[AnyField], Any]) -> Any:
@@ -98,19 +108,26 @@ def get_field_constraints(cls, field: AnyField) -> KwargDefinition:
98108
# add choices as enum. if field is an enum type, we hand this off to
99109
# Litestar for native enum support
100110
if field.choices and not (EnumField and isinstance(field, EnumField)):
101-
constraints["enum"] = [c[0] for c in field.choices]
111+
choices = field.choices
112+
if isinstance(choices, Mapping):
113+
constraints["enum"] = list(choices.keys())
114+
else:
115+
constraints["enum"] = [c[0] for c in choices]
102116

103117
for validator in field.validators:
104118
# fast path for known supported validators
105-
if isinstance(validator, validators.MinValueValidator):
106-
constraints["gt"] = validator.limit_value
107-
elif isinstance(validator, validators.MinLengthValidator):
108-
constraints["min_length"] = validator.limit_value
109-
elif isinstance(validator, validators.MaxValueValidator):
110-
constraints["lt"] = validator.limit_value
111-
elif isinstance(validator, validators.MaxLengthValidator):
112-
constraints["max_length"] = validator.limit_value
113-
elif isinstance(validator, validators.RegexValidator):
119+
# nullable fields do not support these constraints and for enum the
120+
# constraint is defined implicitly by its values
121+
if not field.null and "enum" not in constraints:
122+
if isinstance(validator, validators.MinValueValidator):
123+
constraints["gt"] = validator.limit_value
124+
elif isinstance(validator, validators.MinLengthValidator):
125+
constraints["min_length"] = validator.limit_value
126+
elif isinstance(validator, validators.MaxValueValidator):
127+
constraints["lt"] = validator.limit_value
128+
elif isinstance(validator, validators.MaxLengthValidator):
129+
constraints["max_length"] = validator.limit_value
130+
if isinstance(validator, validators.RegexValidator):
114131
if validator.inverse_match:
115132
if (
116133
isinstance(cls.config, DjangoDTOConfig)
@@ -151,7 +168,7 @@ def create_constraints_for_validator(
151168
@classmethod
152169
def get_field_default(
153170
cls, field: AnyField
154-
) -> tuple[Any, Callable[..., Any] | None]:
171+
) -> tuple[Any, Optional[Callable[..., Any]]]:
155172
if isinstance(field, ForeignObjectRel):
156173
if isinstance(field, ManyToOneRel):
157174
return Empty, list

litestar_django/types.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
from django.contrib.contenttypes.fields import GenericForeignKey # type: ignore[import-untyped]
2-
from django.db.models import Field, ForeignObjectRel # type: ignore[import-untyped]
1+
from __future__ import annotations
32

4-
AnyField = Field | ForeignObjectRel | GenericForeignKey
3+
from typing import TYPE_CHECKING
4+
5+
if TYPE_CHECKING:
6+
from typing_extensions import TypeAlias
7+
from django.contrib.contenttypes.fields import GenericForeignKey # type: ignore[import-untyped]
8+
from django.db.models import Field, ForeignObjectRel # type: ignore[import-untyped]
9+
10+
AnyField: TypeAlias = "Field | ForeignObjectRel | GenericForeignKey"

pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ dependencies = [
2020
dev = [
2121
"django-enumfields2>=3.0.2",
2222
"mypy>=1.15.0",
23+
"pre-commit>=4.2.0",
2324
"pytest>=8.3.5",
2425
"pytest-django>=4.11.1",
2526
"ruff>=0.11.7",
27+
"typing-extensions>=4.13.2",
2628
]
2729

2830
[tool.uv.sources]
@@ -32,3 +34,8 @@ litestar = { git = "https://github.com/litestar-org/litestar", branch = "main" }
3234
[tool.mypy]
3335
packages = ["litestar_django"]
3436
strict = true
37+
python_version = "3.9"
38+
39+
40+
[tool.ruff]
41+
target-version = "py39"

tests/conftest.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import os
22

3-
import pytest
43
import django
54

65

7-
86
os.environ["DJANGO_SETTINGS_MODULE"] = "tests.some_app.settings"
97

108
django.setup()
119

1210
# @pytest.fixture(scope="session", autouse=True)
1311
# def django_setup(monkeypatch) -> None:
14-

tests/manage.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
#!/usr/bin/env python
22
"""Django's command-line utility for administrative tasks."""
3+
34
import os
45
import sys
56

67

78
def main():
89
"""Run administrative tasks."""
9-
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testapp.settings')
10+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings")
1011
try:
1112
from django.core.management import execute_from_command_line
1213
except ImportError as exc:
@@ -18,5 +19,5 @@ def main():
1819
execute_from_command_line(sys.argv)
1920

2021

21-
if __name__ == '__main__':
22+
if __name__ == "__main__":
2223
main()

tests/some_app/app/apps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33

44
class AppConfig(AppConfig):
5-
default_auto_field = 'django.db.models.BigAutoField'
6-
name = 'tests.some_app.app'
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "tests.some_app.app"

0 commit comments

Comments
 (0)