django-ninja-jsonapi 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_ninja_jsonapi-0.2.0/PKG-INFO +174 -0
- django_ninja_jsonapi-0.2.0/README.md +161 -0
- django_ninja_jsonapi-0.2.0/pyproject.toml +49 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/VERSION +1 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/__init__.py +34 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/api/__init__.py +0 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/api/application_builder.py +216 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/api/endpoint_builder.py +117 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/api/schemas.py +26 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/atomic/__init__.py +7 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/atomic/atomic.py +49 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/atomic/atomic_handler.py +176 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/atomic/prepared_atomic_operation.py +342 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/atomic/schemas.py +220 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/common.py +16 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/__init__.py +0 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/base.py +504 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/django_orm/__init__.py +3 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/django_orm/base_model.py +44 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/django_orm/orm.py +469 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/django_orm/query_building.py +84 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/fields/__init__.py +1 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/fields/enums.py +11 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/fields/mixins.py +33 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_typing.py +6 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/exceptions/__init__.py +39 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/exceptions/base.py +29 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/exceptions/handlers.py +12 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/exceptions/json_api.py +192 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/generics.py +3 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/__init__.py +0 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/django_orm/__init__.py +0 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/django_orm/generics/__init__.py +3 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/django_orm/generics/base.py +6 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/generics/__init__.py +3 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/generics/base.py +3 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/py.typed +0 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/querystring.py +286 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/renderers.py +7 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/schema.py +274 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/schema_base.py +36 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/schema_builder.py +536 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/storages/__init__.py +9 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/storages/models_storage.py +105 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/storages/schemas_storage.py +191 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/storages/views_storage.py +30 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/types_metadata/__init__.py +7 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/types_metadata/client_can_set_id.py +7 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/types_metadata/relationship_info.py +11 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/utils/__init__.py +0 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/utils/exceptions.py +20 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/utils/metadata_instance_search.py +23 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/validation_utils.py +51 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/views/__init__.py +19 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/views/enums.py +32 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/views/schemas.py +22 -0
- django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/views/view_base.py +676 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: django-ninja-jsonapi
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: JSON:API toolkit for Django Ninja
|
|
5
|
+
Author: Ignace Maes
|
|
6
|
+
Author-email: Ignace Maes <10243652+IgnaceMaes@users.noreply.github.com>
|
|
7
|
+
Requires-Dist: django>=4.2
|
|
8
|
+
Requires-Dist: django-ninja>=1.0
|
|
9
|
+
Requires-Dist: orjson>=3.10.0
|
|
10
|
+
Requires-Dist: pydantic>=2.6.0
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# django-ninja-jsonapi
|
|
15
|
+
|
|
16
|
+
JSON:API toolkit for Django Ninja.
|
|
17
|
+
|
|
18
|
+
[](https://github.com/ignacemaes/django-ninja-jsonapi/actions/workflows/ci.yml)
|
|
19
|
+
[](https://github.com/ignacemaes/django-ninja-jsonapi/actions/workflows/package.yml)
|
|
20
|
+
|
|
21
|
+
This project ports the core ideas of `fastapi-jsonapi` to a Django Ninja + Django ORM stack.
|
|
22
|
+
|
|
23
|
+
Full documentation is available in [docs/index.md](docs/index.md).
|
|
24
|
+
|
|
25
|
+
## Status
|
|
26
|
+
|
|
27
|
+
- Working baseline for resource registration and route generation (`GET`, `GET LIST`, `POST`, `PATCH`, `DELETE`).
|
|
28
|
+
- Strict query parsing for JSON:API-style `filter`, `sort`, `include`, `fields`, and `page` parameters.
|
|
29
|
+
- JSON:API exception payload handling.
|
|
30
|
+
- Atomic operations endpoint wiring (`/operations`).
|
|
31
|
+
- Django ORM data-layer baseline for CRUD + basic relationship handling.
|
|
32
|
+
- Top-level/resource/relationship `links` in responses.
|
|
33
|
+
- Django ORM include optimization (`select_related`/`prefetch_related` split) with optional include mapping overrides.
|
|
34
|
+
- Logical filter groups (`and`/`or`/`not`) and cursor pagination (`page[cursor]`).
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Python 3.10+
|
|
39
|
+
- Django 4.2+
|
|
40
|
+
- Django Ninja 1.0+
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv add django-ninja-jsonapi
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
or
|
|
49
|
+
|
|
50
|
+
- `pip install django-ninja-jsonapi`
|
|
51
|
+
- `poetry add django-ninja-jsonapi`
|
|
52
|
+
- `pdm add django-ninja-jsonapi`
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
### 1) Define a Django model and a schema
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from django.db import models
|
|
60
|
+
from pydantic import BaseModel
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Customer(models.Model):
|
|
64
|
+
name = models.CharField(max_length=128)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CustomerSchema(BaseModel):
|
|
68
|
+
name: str
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2) Create a JSON:API view class
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from django_ninja_jsonapi import ViewBaseGeneric
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CustomerView(ViewBaseGeneric):
|
|
78
|
+
pass
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3) Register resources with `ApplicationBuilder`
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from ninja import NinjaAPI
|
|
85
|
+
|
|
86
|
+
from django_ninja_jsonapi import ApplicationBuilder
|
|
87
|
+
|
|
88
|
+
api = NinjaAPI()
|
|
89
|
+
builder = ApplicationBuilder(api)
|
|
90
|
+
|
|
91
|
+
builder.add_resource(
|
|
92
|
+
path="/customers",
|
|
93
|
+
tags=["customers"],
|
|
94
|
+
resource_type="customer",
|
|
95
|
+
view=CustomerView,
|
|
96
|
+
model=Customer,
|
|
97
|
+
schema=CustomerSchema,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
builder.initialize()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 4) Mount API in Django URLs
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from django.urls import path
|
|
107
|
+
from .api import api
|
|
108
|
+
|
|
109
|
+
urlpatterns = [
|
|
110
|
+
path("api/", api.urls),
|
|
111
|
+
]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
Set JSON:API options in Django settings:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
NINJA_JSONAPI = {
|
|
120
|
+
"MAX_INCLUDE_DEPTH": 3,
|
|
121
|
+
"MAX_PAGE_SIZE": 100,
|
|
122
|
+
"ALLOW_DISABLE_PAGINATION": True,
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Exported public API
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from django_ninja_jsonapi import ApplicationBuilder, QueryStringManager, HTTPException, BadRequest, ViewBaseGeneric
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
uv run ruff format src tests
|
|
136
|
+
uv run ruff check src tests
|
|
137
|
+
uv run pytest --cov=src/django_ninja_jsonapi --cov-report=term-missing
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contribution workflow.
|
|
141
|
+
|
|
142
|
+
## Release process
|
|
143
|
+
|
|
144
|
+
Releases are automated with GitHub Actions:
|
|
145
|
+
|
|
146
|
+
1. Merge conventional-commit PRs into `main`.
|
|
147
|
+
2. `Release Please` opens or updates a release PR with version bump + changelog updates.
|
|
148
|
+
3. Merge the release PR to create a GitHub Release.
|
|
149
|
+
4. `Publish to PyPI` runs on `release: published` and uploads the built package to PyPI.
|
|
150
|
+
|
|
151
|
+
Workflows:
|
|
152
|
+
|
|
153
|
+
- `.github/workflows/release-please.yml`
|
|
154
|
+
- `.github/workflows/publish.yml`
|
|
155
|
+
|
|
156
|
+
Required repository secrets:
|
|
157
|
+
|
|
158
|
+
- `REPO_ADMIN_TOKEN` (used by Release Please)
|
|
159
|
+
- `PYPI_API_TOKEN` (used for PyPI publishing)
|
|
160
|
+
|
|
161
|
+
## Test coverage
|
|
162
|
+
|
|
163
|
+
Current tests cover:
|
|
164
|
+
|
|
165
|
+
- Application builder initialization and route registration behavior
|
|
166
|
+
- Query-string parsing behavior
|
|
167
|
+
- Django ORM query-building mapping (`filter`/`sort` translation)
|
|
168
|
+
- Exception handler response shape
|
|
169
|
+
|
|
170
|
+
## Notes
|
|
171
|
+
|
|
172
|
+
- This project is Django Ninja + Django ORM focused.
|
|
173
|
+
- SQLAlchemy-specific modules have been removed to keep the codebase simpler and consistent.
|
|
174
|
+
- CI runs on pull requests and pushes to `main`.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# django-ninja-jsonapi
|
|
2
|
+
|
|
3
|
+
JSON:API toolkit for Django Ninja.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/ignacemaes/django-ninja-jsonapi/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/ignacemaes/django-ninja-jsonapi/actions/workflows/package.yml)
|
|
7
|
+
|
|
8
|
+
This project ports the core ideas of `fastapi-jsonapi` to a Django Ninja + Django ORM stack.
|
|
9
|
+
|
|
10
|
+
Full documentation is available in [docs/index.md](docs/index.md).
|
|
11
|
+
|
|
12
|
+
## Status
|
|
13
|
+
|
|
14
|
+
- Working baseline for resource registration and route generation (`GET`, `GET LIST`, `POST`, `PATCH`, `DELETE`).
|
|
15
|
+
- Strict query parsing for JSON:API-style `filter`, `sort`, `include`, `fields`, and `page` parameters.
|
|
16
|
+
- JSON:API exception payload handling.
|
|
17
|
+
- Atomic operations endpoint wiring (`/operations`).
|
|
18
|
+
- Django ORM data-layer baseline for CRUD + basic relationship handling.
|
|
19
|
+
- Top-level/resource/relationship `links` in responses.
|
|
20
|
+
- Django ORM include optimization (`select_related`/`prefetch_related` split) with optional include mapping overrides.
|
|
21
|
+
- Logical filter groups (`and`/`or`/`not`) and cursor pagination (`page[cursor]`).
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Python 3.10+
|
|
26
|
+
- Django 4.2+
|
|
27
|
+
- Django Ninja 1.0+
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv add django-ninja-jsonapi
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
or
|
|
36
|
+
|
|
37
|
+
- `pip install django-ninja-jsonapi`
|
|
38
|
+
- `poetry add django-ninja-jsonapi`
|
|
39
|
+
- `pdm add django-ninja-jsonapi`
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
### 1) Define a Django model and a schema
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from django.db import models
|
|
47
|
+
from pydantic import BaseModel
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Customer(models.Model):
|
|
51
|
+
name = models.CharField(max_length=128)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CustomerSchema(BaseModel):
|
|
55
|
+
name: str
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2) Create a JSON:API view class
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from django_ninja_jsonapi import ViewBaseGeneric
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CustomerView(ViewBaseGeneric):
|
|
65
|
+
pass
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3) Register resources with `ApplicationBuilder`
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from ninja import NinjaAPI
|
|
72
|
+
|
|
73
|
+
from django_ninja_jsonapi import ApplicationBuilder
|
|
74
|
+
|
|
75
|
+
api = NinjaAPI()
|
|
76
|
+
builder = ApplicationBuilder(api)
|
|
77
|
+
|
|
78
|
+
builder.add_resource(
|
|
79
|
+
path="/customers",
|
|
80
|
+
tags=["customers"],
|
|
81
|
+
resource_type="customer",
|
|
82
|
+
view=CustomerView,
|
|
83
|
+
model=Customer,
|
|
84
|
+
schema=CustomerSchema,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
builder.initialize()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 4) Mount API in Django URLs
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from django.urls import path
|
|
94
|
+
from .api import api
|
|
95
|
+
|
|
96
|
+
urlpatterns = [
|
|
97
|
+
path("api/", api.urls),
|
|
98
|
+
]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
Set JSON:API options in Django settings:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
NINJA_JSONAPI = {
|
|
107
|
+
"MAX_INCLUDE_DEPTH": 3,
|
|
108
|
+
"MAX_PAGE_SIZE": 100,
|
|
109
|
+
"ALLOW_DISABLE_PAGINATION": True,
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Exported public API
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from django_ninja_jsonapi import ApplicationBuilder, QueryStringManager, HTTPException, BadRequest, ViewBaseGeneric
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
uv run ruff format src tests
|
|
123
|
+
uv run ruff check src tests
|
|
124
|
+
uv run pytest --cov=src/django_ninja_jsonapi --cov-report=term-missing
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contribution workflow.
|
|
128
|
+
|
|
129
|
+
## Release process
|
|
130
|
+
|
|
131
|
+
Releases are automated with GitHub Actions:
|
|
132
|
+
|
|
133
|
+
1. Merge conventional-commit PRs into `main`.
|
|
134
|
+
2. `Release Please` opens or updates a release PR with version bump + changelog updates.
|
|
135
|
+
3. Merge the release PR to create a GitHub Release.
|
|
136
|
+
4. `Publish to PyPI` runs on `release: published` and uploads the built package to PyPI.
|
|
137
|
+
|
|
138
|
+
Workflows:
|
|
139
|
+
|
|
140
|
+
- `.github/workflows/release-please.yml`
|
|
141
|
+
- `.github/workflows/publish.yml`
|
|
142
|
+
|
|
143
|
+
Required repository secrets:
|
|
144
|
+
|
|
145
|
+
- `REPO_ADMIN_TOKEN` (used by Release Please)
|
|
146
|
+
- `PYPI_API_TOKEN` (used for PyPI publishing)
|
|
147
|
+
|
|
148
|
+
## Test coverage
|
|
149
|
+
|
|
150
|
+
Current tests cover:
|
|
151
|
+
|
|
152
|
+
- Application builder initialization and route registration behavior
|
|
153
|
+
- Query-string parsing behavior
|
|
154
|
+
- Django ORM query-building mapping (`filter`/`sort` translation)
|
|
155
|
+
- Exception handler response shape
|
|
156
|
+
|
|
157
|
+
## Notes
|
|
158
|
+
|
|
159
|
+
- This project is Django Ninja + Django ORM focused.
|
|
160
|
+
- SQLAlchemy-specific modules have been removed to keep the codebase simpler and consistent.
|
|
161
|
+
- CI runs on pull requests and pushes to `main`.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "django-ninja-jsonapi"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "JSON:API toolkit for Django Ninja"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Ignace Maes", email = "10243652+IgnaceMaes@users.noreply.github.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"django>=4.2",
|
|
12
|
+
"django-ninja>=1.0",
|
|
13
|
+
"orjson>=3.10.0",
|
|
14
|
+
"pydantic>=2.6.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.10.3,<0.11.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=9.0.2",
|
|
24
|
+
"pytest-asyncio>=1.3.0",
|
|
25
|
+
"pytest-cov>=6.0.0",
|
|
26
|
+
"pytest-django>=4.12.0",
|
|
27
|
+
"ruff>=0.15.2",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.ruff]
|
|
31
|
+
target-version = "py310"
|
|
32
|
+
line-length = 120
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint]
|
|
35
|
+
select = ["E", "F", "I", "B"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
DJANGO_SETTINGS_MODULE = "tests.settings"
|
|
39
|
+
pythonpath = [".", "src"]
|
|
40
|
+
django_find_project = false
|
|
41
|
+
|
|
42
|
+
[tool.coverage.run]
|
|
43
|
+
branch = true
|
|
44
|
+
source = ["src/django_ninja_jsonapi"]
|
|
45
|
+
|
|
46
|
+
[tool.coverage.report]
|
|
47
|
+
show_missing = true
|
|
48
|
+
skip_covered = true
|
|
49
|
+
fail_under = 45
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""JSON API utils package."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from django_ninja_jsonapi.exceptions import BadRequest
|
|
7
|
+
from django_ninja_jsonapi.exceptions.json_api import HTTPException
|
|
8
|
+
from django_ninja_jsonapi.querystring import QueryStringManager
|
|
9
|
+
from django_ninja_jsonapi.renderers import JSONAPIRenderer
|
|
10
|
+
|
|
11
|
+
__version__ = Path(__file__).parent.joinpath("VERSION").read_text().strip()
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ApplicationBuilder",
|
|
15
|
+
"BadRequest",
|
|
16
|
+
"HTTPException",
|
|
17
|
+
"JSONAPIRenderer",
|
|
18
|
+
"QueryStringManager",
|
|
19
|
+
"ViewBaseGeneric",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def __getattr__(name: str) -> Any:
|
|
24
|
+
if name == "ApplicationBuilder":
|
|
25
|
+
from django_ninja_jsonapi.api.application_builder import ApplicationBuilder
|
|
26
|
+
|
|
27
|
+
return ApplicationBuilder
|
|
28
|
+
|
|
29
|
+
if name == "ViewBaseGeneric":
|
|
30
|
+
from django_ninja_jsonapi.generics import ViewBaseGeneric
|
|
31
|
+
|
|
32
|
+
return ViewBaseGeneric
|
|
33
|
+
|
|
34
|
+
raise AttributeError(name)
|
|
File without changes
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from typing import Any, Callable, Iterable, Optional, Type
|
|
3
|
+
|
|
4
|
+
from ninja import NinjaAPI, Router
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from django_ninja_jsonapi.api.endpoint_builder import EndpointsBuilder
|
|
8
|
+
from django_ninja_jsonapi.api.schemas import ResourceData
|
|
9
|
+
from django_ninja_jsonapi.atomic.atomic import AtomicOperations
|
|
10
|
+
from django_ninja_jsonapi.data_typing import TypeModel
|
|
11
|
+
from django_ninja_jsonapi.exceptions import HTTPException
|
|
12
|
+
from django_ninja_jsonapi.exceptions.handlers import base_exception_handler
|
|
13
|
+
from django_ninja_jsonapi.renderers import JSONAPIRenderer
|
|
14
|
+
from django_ninja_jsonapi.schema_builder import SchemaBuilder
|
|
15
|
+
from django_ninja_jsonapi.storages.models_storage import models_storage
|
|
16
|
+
from django_ninja_jsonapi.storages.schemas_storage import schemas_storage
|
|
17
|
+
from django_ninja_jsonapi.storages.views_storage import views_storage
|
|
18
|
+
from django_ninja_jsonapi.views.enums import Operation
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ApplicationBuilderError(Exception):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ApplicationBuilder:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
api: NinjaAPI,
|
|
29
|
+
base_router: Optional[Router] = None,
|
|
30
|
+
exception_handler: Optional[Callable] = None,
|
|
31
|
+
**base_router_include_kwargs,
|
|
32
|
+
):
|
|
33
|
+
self._api = api
|
|
34
|
+
self._base_router = base_router or Router()
|
|
35
|
+
self._base_router_include_kwargs = base_router_include_kwargs
|
|
36
|
+
self._routers: dict[str, Router] = {}
|
|
37
|
+
self._router_include_kwargs: dict[str, dict[str, Any]] = {}
|
|
38
|
+
self._resource_data: dict[str, ResourceData] = {}
|
|
39
|
+
self._exception_handler: Callable = exception_handler or base_exception_handler
|
|
40
|
+
self._initialized = False
|
|
41
|
+
self._api.renderer = JSONAPIRenderer()
|
|
42
|
+
|
|
43
|
+
def add_resource(
|
|
44
|
+
self,
|
|
45
|
+
path: str,
|
|
46
|
+
tags: Iterable[str],
|
|
47
|
+
resource_type: str,
|
|
48
|
+
view: Type[Any],
|
|
49
|
+
model: Type[TypeModel],
|
|
50
|
+
schema: Type[BaseModel],
|
|
51
|
+
router: Optional[Router] = None,
|
|
52
|
+
schema_in_post: Optional[Type[BaseModel]] = None,
|
|
53
|
+
schema_in_patch: Optional[Type[BaseModel]] = None,
|
|
54
|
+
pagination_default_size: Optional[int] = 25,
|
|
55
|
+
pagination_default_number: Optional[int] = 1,
|
|
56
|
+
pagination_default_offset: Optional[int] = None,
|
|
57
|
+
pagination_default_limit: Optional[int] = None,
|
|
58
|
+
operations: Iterable[Operation] = (),
|
|
59
|
+
ending_slash: bool = True,
|
|
60
|
+
model_id_field_name: str = "id",
|
|
61
|
+
include_router_kwargs: Optional[dict] = None,
|
|
62
|
+
):
|
|
63
|
+
if self._initialized:
|
|
64
|
+
raise ApplicationBuilderError("Can't add resource after app initialization")
|
|
65
|
+
|
|
66
|
+
if resource_type in self._resource_data:
|
|
67
|
+
raise ApplicationBuilderError(f"Resource {resource_type!r} already registered")
|
|
68
|
+
|
|
69
|
+
if include_router_kwargs is not None and router is None:
|
|
70
|
+
raise ApplicationBuilderError(
|
|
71
|
+
"The argument 'include_router_kwargs' is not allowed when 'router' is missing"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
models_storage.add_model(resource_type, model, model_id_field_name, path)
|
|
75
|
+
views_storage.add_view(resource_type, view)
|
|
76
|
+
|
|
77
|
+
dto = SchemaBuilder(resource_type).create_schemas(
|
|
78
|
+
schema=schema,
|
|
79
|
+
schema_in_post=schema_in_post,
|
|
80
|
+
schema_in_patch=schema_in_patch,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
resource_operations = list(operations) or Operation.real_operations()
|
|
84
|
+
if Operation.ALL in resource_operations:
|
|
85
|
+
resource_operations = Operation.real_operations()
|
|
86
|
+
|
|
87
|
+
self._resource_data[resource_type] = ResourceData(
|
|
88
|
+
path=path,
|
|
89
|
+
tags=list(tags),
|
|
90
|
+
view=view,
|
|
91
|
+
model=model,
|
|
92
|
+
source_schema=schema,
|
|
93
|
+
schema_in_post=schema_in_post,
|
|
94
|
+
schema_in_post_data=dto.schema_in_post_data,
|
|
95
|
+
schema_in_patch=schema_in_patch,
|
|
96
|
+
schema_in_patch_data=dto.schema_in_patch_data,
|
|
97
|
+
detail_response_schema=dto.detail_response_schema,
|
|
98
|
+
list_response_schema=dto.list_response_schema,
|
|
99
|
+
pagination_default_size=pagination_default_size,
|
|
100
|
+
pagination_default_number=pagination_default_number,
|
|
101
|
+
pagination_default_offset=pagination_default_offset,
|
|
102
|
+
pagination_default_limit=pagination_default_limit,
|
|
103
|
+
operations=resource_operations,
|
|
104
|
+
ending_slash=ending_slash,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
resolved_router = router or self._base_router
|
|
108
|
+
self._routers[resource_type] = resolved_router
|
|
109
|
+
self._router_include_kwargs[resource_type] = include_router_kwargs or {}
|
|
110
|
+
|
|
111
|
+
def initialize(self) -> NinjaAPI:
|
|
112
|
+
if self._initialized:
|
|
113
|
+
raise ApplicationBuilderError("Application already initialized")
|
|
114
|
+
|
|
115
|
+
self._initialized = True
|
|
116
|
+
self._register_exception_handler()
|
|
117
|
+
|
|
118
|
+
for resource_type, data in self._resource_data.items():
|
|
119
|
+
builder = EndpointsBuilder(resource_type, data)
|
|
120
|
+
router = self._routers[resource_type]
|
|
121
|
+
|
|
122
|
+
for operation in data.operations:
|
|
123
|
+
name, endpoint = builder.create_common_ninja_endpoint(operation)
|
|
124
|
+
method = operation.http_method().lower()
|
|
125
|
+
path = self._create_path(
|
|
126
|
+
path=data.path,
|
|
127
|
+
include_object_id=operation in {Operation.GET, Operation.UPDATE, Operation.DELETE},
|
|
128
|
+
ending_slash=data.ending_slash,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
response_schema = self._response_for(data, operation)
|
|
132
|
+
route = getattr(router, method)
|
|
133
|
+
route(
|
|
134
|
+
path,
|
|
135
|
+
response=response_schema,
|
|
136
|
+
tags=data.tags,
|
|
137
|
+
operation_id=name,
|
|
138
|
+
)(endpoint)
|
|
139
|
+
|
|
140
|
+
relationships_info = schemas_storage.get_relationships_info(
|
|
141
|
+
resource_type=resource_type,
|
|
142
|
+
operation_type="get",
|
|
143
|
+
)
|
|
144
|
+
for relationship_name, info in relationships_info.items():
|
|
145
|
+
if not views_storage.has_view(info.resource_type):
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
operation = Operation.GET_LIST if info.many else Operation.GET
|
|
149
|
+
relationship_name_id, relationship_endpoint = builder.create_relationship_endpoint(
|
|
150
|
+
parent_resource_type=resource_type,
|
|
151
|
+
relationship_name=relationship_name,
|
|
152
|
+
operation=operation,
|
|
153
|
+
)
|
|
154
|
+
relationship_path = self._create_relationship_path(
|
|
155
|
+
resource_path=data.path,
|
|
156
|
+
relationship_name=relationship_name,
|
|
157
|
+
ending_slash=data.ending_slash,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
related_data = self._resource_data.get(info.resource_type, data)
|
|
161
|
+
relationship_response = (
|
|
162
|
+
related_data.list_response_schema if info.many else related_data.detail_response_schema
|
|
163
|
+
)
|
|
164
|
+
getattr(router, operation.http_method().lower())(
|
|
165
|
+
relationship_path,
|
|
166
|
+
response=relationship_response,
|
|
167
|
+
tags=data.tags,
|
|
168
|
+
operation_id=relationship_name_id,
|
|
169
|
+
)(relationship_endpoint)
|
|
170
|
+
|
|
171
|
+
registered_routers = set()
|
|
172
|
+
for resource_type, router in self._routers.items():
|
|
173
|
+
if id(router) in registered_routers:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
include_kwargs = self._router_include_kwargs.get(resource_type, {})
|
|
177
|
+
if router is self._base_router:
|
|
178
|
+
include_kwargs = self._base_router_include_kwargs
|
|
179
|
+
|
|
180
|
+
self._api.add_router("", router, **include_kwargs)
|
|
181
|
+
registered_routers.add(id(router))
|
|
182
|
+
|
|
183
|
+
atomic = AtomicOperations()
|
|
184
|
+
self._api.add_router("", atomic.router)
|
|
185
|
+
|
|
186
|
+
return self._api
|
|
187
|
+
|
|
188
|
+
def _register_exception_handler(self):
|
|
189
|
+
add_handler = getattr(self._api, "add_exception_handler", None)
|
|
190
|
+
if callable(add_handler):
|
|
191
|
+
add_handler(HTTPException, self._exception_handler)
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _response_for(data: ResourceData, operation: Operation):
|
|
195
|
+
if operation == Operation.DELETE:
|
|
196
|
+
return {HTTPStatus.NO_CONTENT: None}
|
|
197
|
+
if operation == Operation.GET_LIST:
|
|
198
|
+
return data.list_response_schema
|
|
199
|
+
return data.detail_response_schema
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _create_path(path: str, include_object_id: bool, ending_slash: bool) -> str:
|
|
203
|
+
base_path = path.rstrip("/")
|
|
204
|
+
if include_object_id:
|
|
205
|
+
base_path = f"{base_path}/{{obj_id}}"
|
|
206
|
+
if ending_slash:
|
|
207
|
+
return f"{base_path}/"
|
|
208
|
+
return base_path
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def _create_relationship_path(resource_path: str, relationship_name: str, ending_slash: bool) -> str:
|
|
212
|
+
base_path = resource_path.rstrip("/")
|
|
213
|
+
path = f"{base_path}/{{obj_id}}/relationships/{relationship_name}"
|
|
214
|
+
if ending_slash:
|
|
215
|
+
return f"{path}/"
|
|
216
|
+
return path
|