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.
Files changed (57) hide show
  1. django_ninja_jsonapi-0.2.0/PKG-INFO +174 -0
  2. django_ninja_jsonapi-0.2.0/README.md +161 -0
  3. django_ninja_jsonapi-0.2.0/pyproject.toml +49 -0
  4. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/VERSION +1 -0
  5. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/__init__.py +34 -0
  6. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/api/__init__.py +0 -0
  7. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/api/application_builder.py +216 -0
  8. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/api/endpoint_builder.py +117 -0
  9. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/api/schemas.py +26 -0
  10. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/atomic/__init__.py +7 -0
  11. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/atomic/atomic.py +49 -0
  12. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/atomic/atomic_handler.py +176 -0
  13. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/atomic/prepared_atomic_operation.py +342 -0
  14. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/atomic/schemas.py +220 -0
  15. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/common.py +16 -0
  16. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/__init__.py +0 -0
  17. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/base.py +504 -0
  18. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/django_orm/__init__.py +3 -0
  19. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/django_orm/base_model.py +44 -0
  20. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/django_orm/orm.py +469 -0
  21. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/django_orm/query_building.py +84 -0
  22. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/fields/__init__.py +1 -0
  23. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/fields/enums.py +11 -0
  24. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_layers/fields/mixins.py +33 -0
  25. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/data_typing.py +6 -0
  26. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/exceptions/__init__.py +39 -0
  27. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/exceptions/base.py +29 -0
  28. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/exceptions/handlers.py +12 -0
  29. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/exceptions/json_api.py +192 -0
  30. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/generics.py +3 -0
  31. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/__init__.py +0 -0
  32. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/django_orm/__init__.py +0 -0
  33. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/django_orm/generics/__init__.py +3 -0
  34. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/django_orm/generics/base.py +6 -0
  35. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/generics/__init__.py +3 -0
  36. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/misc/generics/base.py +3 -0
  37. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/py.typed +0 -0
  38. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/querystring.py +286 -0
  39. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/renderers.py +7 -0
  40. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/schema.py +274 -0
  41. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/schema_base.py +36 -0
  42. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/schema_builder.py +536 -0
  43. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/storages/__init__.py +9 -0
  44. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/storages/models_storage.py +105 -0
  45. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/storages/schemas_storage.py +191 -0
  46. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/storages/views_storage.py +30 -0
  47. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/types_metadata/__init__.py +7 -0
  48. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/types_metadata/client_can_set_id.py +7 -0
  49. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/types_metadata/relationship_info.py +11 -0
  50. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/utils/__init__.py +0 -0
  51. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/utils/exceptions.py +20 -0
  52. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/utils/metadata_instance_search.py +23 -0
  53. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/validation_utils.py +51 -0
  54. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/views/__init__.py +19 -0
  55. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/views/enums.py +32 -0
  56. django_ninja_jsonapi-0.2.0/src/django_ninja_jsonapi/views/schemas.py +22 -0
  57. 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
+ [![CI](https://github.com/ignacemaes/django-ninja-jsonapi/actions/workflows/ci.yml/badge.svg)](https://github.com/ignacemaes/django-ninja-jsonapi/actions/workflows/ci.yml)
19
+ [![Package](https://github.com/ignacemaes/django-ninja-jsonapi/actions/workflows/package.yml/badge.svg)](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
+ [![CI](https://github.com/ignacemaes/django-ninja-jsonapi/actions/workflows/ci.yml/badge.svg)](https://github.com/ignacemaes/django-ninja-jsonapi/actions/workflows/ci.yml)
6
+ [![Package](https://github.com/ignacemaes/django-ninja-jsonapi/actions/workflows/package.yml/badge.svg)](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,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)
@@ -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