django-ninja-aio-crud 2.13.0__tar.gz → 2.15.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.
Potentially problematic release.
This version of django-ninja-aio-crud might be problematic. Click here for more details.
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.github/workflows/docs.yml +6 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/PKG-INFO +1 -1
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/models/model_serializer.md +159 -5
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/models/serializers.md +165 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/models/serializers.py +147 -13
- django_ninja_aio_crud-2.15.0/tests/test_app/models.py +478 -0
- django_ninja_aio_crud-2.15.0/tests/test_app/serializers.py +118 -0
- django_ninja_aio_crud-2.15.0/tests/test_serializers.py +1353 -0
- django_ninja_aio_crud-2.13.0/tests/test_app/models.py +0 -225
- django_ninja_aio_crud-2.13.0/tests/test_app/serializers.py +0 -47
- django_ninja_aio_crud-2.13.0/tests/test_serializers.py +0 -600
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.gitignore +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/LICENSE +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/README.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/api_view.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/api_view_set.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/decorators.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/views/mixins.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/main.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/mkdocs.yml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/decorators/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/decorators/operations.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/decorators/views.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/factory/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/factory/operations.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/helpers/api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/models/utils.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/filters.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/schemas/helpers.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/views/api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/views/mixins.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/helpers/test_many_to_many_api.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_app/views.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_auth.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/views/test_views.py +0 -0
- {django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/tests/views/test_viewset.py +0 -0
|
@@ -25,6 +25,9 @@ on:
|
|
|
25
25
|
- "2.10"
|
|
26
26
|
- "2.11"
|
|
27
27
|
- "2.12"
|
|
28
|
+
- "2.13"
|
|
29
|
+
- "2.14"
|
|
30
|
+
- "2.15"
|
|
28
31
|
make_latest:
|
|
29
32
|
description: 'Set as "latest" and default?'
|
|
30
33
|
type: boolean
|
|
@@ -53,6 +56,9 @@ on:
|
|
|
53
56
|
- "2.10"
|
|
54
57
|
- "2.11"
|
|
55
58
|
- "2.12"
|
|
59
|
+
- "2.13"
|
|
60
|
+
- "2.14"
|
|
61
|
+
- "2.15"
|
|
56
62
|
delete_confirm:
|
|
57
63
|
description: 'Confirm deletion of the selected version'
|
|
58
64
|
type: boolean
|
{django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/models/model_serializer.md
RENAMED
|
@@ -100,11 +100,12 @@ Describes how to build a read (output) schema for a model.
|
|
|
100
100
|
|
|
101
101
|
**Attributes**
|
|
102
102
|
|
|
103
|
-
| Attribute
|
|
104
|
-
|
|
105
|
-
| `fields`
|
|
106
|
-
| `excludes`
|
|
107
|
-
| `customs`
|
|
103
|
+
| Attribute | Type | Description |
|
|
104
|
+
|------------------|-----------------|-------------|
|
|
105
|
+
| `fields` | `list[str]` | **REQUIRED.** Model fields / related names explicitly included in the read (output) schema. |
|
|
106
|
+
| `excludes` | `list[str]` | Fields / related names to always omit (takes precedence over `fields` and `optionals`). Use for sensitive or noisy data (e.g., passwords, internal flags). |
|
|
107
|
+
| `customs` | `list[tuple]` | Computed / synthetic output values. Tuple formats:<br>• `(name, type)` = required resolvable attribute (object attribute or property). Serialization error if not resolvable.<br>• `(name, type, default)` = optional; default may be a callable (`lambda obj: ...`) or a literal value. |
|
|
108
|
+
| `relations_as_id`| `list[str]` | Relation fields to serialize as IDs instead of nested objects. Works with forward FK, forward O2O, reverse FK, reverse O2O, and M2M relations. |
|
|
108
109
|
|
|
109
110
|
**Example:**
|
|
110
111
|
|
|
@@ -405,6 +406,159 @@ class Book(ModelSerializer):
|
|
|
405
406
|
}
|
|
406
407
|
```
|
|
407
408
|
|
|
409
|
+
#### Relations as ID
|
|
410
|
+
|
|
411
|
+
Use `relations_as_id` to serialize relation fields as IDs instead of nested objects. This is useful for:
|
|
412
|
+
|
|
413
|
+
- Reducing response payload size
|
|
414
|
+
- Avoiding circular serialization
|
|
415
|
+
- Performance optimization when nested data isn't needed
|
|
416
|
+
- API designs where clients fetch related data separately
|
|
417
|
+
|
|
418
|
+
**Supported Relations:**
|
|
419
|
+
|
|
420
|
+
| Relation Type | Output Type | Example Value |
|
|
421
|
+
|--------------------|-------------------|----------------------|
|
|
422
|
+
| Forward FK | `PK_TYPE \| None` | `5` or `null` |
|
|
423
|
+
| Forward O2O | `PK_TYPE \| None` | `3` or `null` |
|
|
424
|
+
| Reverse FK | `list[PK_TYPE]` | `[1, 2, 3]` |
|
|
425
|
+
| Reverse O2O | `PK_TYPE \| None` | `7` or `null` |
|
|
426
|
+
| M2M (forward) | `list[PK_TYPE]` | `[1, 2]` |
|
|
427
|
+
| M2M (reverse) | `list[PK_TYPE]` | `[4, 5, 6]` |
|
|
428
|
+
|
|
429
|
+
**Note:** `PK_TYPE` is automatically detected from the related model's primary key field. Supported types include `int` (default), `UUID`, `str`, and any other Django primary key type.
|
|
430
|
+
|
|
431
|
+
**Example:**
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
class Author(ModelSerializer):
|
|
435
|
+
name = models.CharField(max_length=200)
|
|
436
|
+
|
|
437
|
+
class ReadSerializer:
|
|
438
|
+
fields = ["id", "name", "books"]
|
|
439
|
+
relations_as_id = ["books"] # Serialize as list of IDs
|
|
440
|
+
|
|
441
|
+
class Book(ModelSerializer):
|
|
442
|
+
title = models.CharField(max_length=200)
|
|
443
|
+
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
|
|
444
|
+
|
|
445
|
+
class ReadSerializer:
|
|
446
|
+
fields = ["id", "title", "author"]
|
|
447
|
+
relations_as_id = ["author"] # Serialize as ID
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Output (Author):**
|
|
451
|
+
|
|
452
|
+
```json
|
|
453
|
+
{
|
|
454
|
+
"id": 1,
|
|
455
|
+
"name": "J.K. Rowling",
|
|
456
|
+
"books": [1, 2, 3]
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Output (Book):**
|
|
461
|
+
|
|
462
|
+
```json
|
|
463
|
+
{
|
|
464
|
+
"id": 1,
|
|
465
|
+
"title": "Harry Potter",
|
|
466
|
+
"author": 1
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**M2M Example:**
|
|
471
|
+
|
|
472
|
+
```python
|
|
473
|
+
class Tag(ModelSerializer):
|
|
474
|
+
name = models.CharField(max_length=50)
|
|
475
|
+
|
|
476
|
+
class ReadSerializer:
|
|
477
|
+
fields = ["id", "name", "articles"]
|
|
478
|
+
relations_as_id = ["articles"] # Reverse M2M as IDs
|
|
479
|
+
|
|
480
|
+
class Article(ModelSerializer):
|
|
481
|
+
title = models.CharField(max_length=200)
|
|
482
|
+
tags = models.ManyToManyField(Tag, related_name="articles")
|
|
483
|
+
|
|
484
|
+
class ReadSerializer:
|
|
485
|
+
fields = ["id", "title", "tags"]
|
|
486
|
+
relations_as_id = ["tags"] # Forward M2M as IDs
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Output (Article):**
|
|
490
|
+
|
|
491
|
+
```json
|
|
492
|
+
{
|
|
493
|
+
"id": 1,
|
|
494
|
+
"title": "Getting Started with Django",
|
|
495
|
+
"tags": [1, 2, 5]
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**UUID Primary Key Example:**
|
|
500
|
+
|
|
501
|
+
When models use UUID primary keys, the output type is automatically `UUID`:
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
import uuid
|
|
505
|
+
from django.db import models
|
|
506
|
+
from ninja_aio.models import ModelSerializer
|
|
507
|
+
|
|
508
|
+
class Author(ModelSerializer):
|
|
509
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
510
|
+
name = models.CharField(max_length=200)
|
|
511
|
+
|
|
512
|
+
class ReadSerializer:
|
|
513
|
+
fields = ["id", "name", "books"]
|
|
514
|
+
relations_as_id = ["books"]
|
|
515
|
+
|
|
516
|
+
class Book(ModelSerializer):
|
|
517
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
518
|
+
title = models.CharField(max_length=200)
|
|
519
|
+
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
|
|
520
|
+
|
|
521
|
+
class ReadSerializer:
|
|
522
|
+
fields = ["id", "title", "author"]
|
|
523
|
+
relations_as_id = ["author"]
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**Output (Author with UUID):**
|
|
527
|
+
|
|
528
|
+
```json
|
|
529
|
+
{
|
|
530
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
531
|
+
"name": "J.K. Rowling",
|
|
532
|
+
"books": [
|
|
533
|
+
"6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
534
|
+
"6ba7b811-9dad-11d1-80b4-00c04fd430c8"
|
|
535
|
+
]
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**Output (Book with UUID):**
|
|
540
|
+
|
|
541
|
+
```json
|
|
542
|
+
{
|
|
543
|
+
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
544
|
+
"title": "Harry Potter",
|
|
545
|
+
"author": "550e8400-e29b-41d4-a716-446655440000"
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Query Optimization Note:** When using `relations_as_id`, you should still use `select_related()` for forward relations and `prefetch_related()` for reverse/M2M relations to avoid N+1 queries:
|
|
550
|
+
|
|
551
|
+
```python
|
|
552
|
+
class Article(ModelSerializer):
|
|
553
|
+
# ...
|
|
554
|
+
|
|
555
|
+
class QuerySet:
|
|
556
|
+
read = ModelQuerySetSchema(
|
|
557
|
+
select_related=["author"], # For forward FK
|
|
558
|
+
prefetch_related=["tags"], # For M2M
|
|
559
|
+
)
|
|
560
|
+
```
|
|
561
|
+
|
|
408
562
|
## Async Extension Points
|
|
409
563
|
|
|
410
564
|
### `queryset_request(request)`
|
{django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/docs/api/models/serializers.md
RENAMED
|
@@ -41,6 +41,7 @@ Define a Serializer subclass with a nested Meta:
|
|
|
41
41
|
- **schema_detail**: SchemaModelConfig for detail outputs (retrieve endpoint)
|
|
42
42
|
- **schema_update**: SchemaModelConfig for patch/update inputs
|
|
43
43
|
- **relations_serializers**: Mapping of relation field name -> Serializer class, **string reference**, or **Union of serializers** (supports forward/circular dependencies and polymorphic relations)
|
|
44
|
+
- **relations_as_id**: List of relation field names to serialize as IDs instead of nested objects
|
|
44
45
|
|
|
45
46
|
SchemaModelConfig fields:
|
|
46
47
|
|
|
@@ -415,6 +416,170 @@ Notes:
|
|
|
415
416
|
- Union types are resolved lazily, so forward and circular references work seamlessly.
|
|
416
417
|
- The schema generator will create a union of all possible schemas from the serializers in the Union.
|
|
417
418
|
|
|
419
|
+
## Relations as ID
|
|
420
|
+
|
|
421
|
+
Use `relations_as_id` in Meta to serialize relation fields as IDs instead of nested objects. This is useful for:
|
|
422
|
+
|
|
423
|
+
- Reducing response payload size
|
|
424
|
+
- Avoiding circular serialization
|
|
425
|
+
- Performance optimization when nested data isn't needed
|
|
426
|
+
- API designs where clients fetch related data separately
|
|
427
|
+
|
|
428
|
+
**Supported Relations:**
|
|
429
|
+
|
|
430
|
+
| Relation Type | Output Type | Example Value |
|
|
431
|
+
|--------------------|-------------------|----------------------|
|
|
432
|
+
| Forward FK | `PK_TYPE \| None` | `5` or `null` |
|
|
433
|
+
| Forward O2O | `PK_TYPE \| None` | `3` or `null` |
|
|
434
|
+
| Reverse FK | `list[PK_TYPE]` | `[1, 2, 3]` |
|
|
435
|
+
| Reverse O2O | `PK_TYPE \| None` | `7` or `null` |
|
|
436
|
+
| M2M (forward) | `list[PK_TYPE]` | `[1, 2]` |
|
|
437
|
+
| M2M (reverse) | `list[PK_TYPE]` | `[4, 5, 6]` |
|
|
438
|
+
|
|
439
|
+
**Note:** `PK_TYPE` is automatically detected from the related model's primary key field. Supported types include `int` (default), `UUID`, `str`, and any other Django primary key type.
|
|
440
|
+
|
|
441
|
+
**Example:**
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
from ninja_aio.models import serializers
|
|
445
|
+
from . import models
|
|
446
|
+
|
|
447
|
+
class AuthorSerializer(serializers.Serializer):
|
|
448
|
+
class Meta:
|
|
449
|
+
model = models.Author
|
|
450
|
+
schema_out = serializers.SchemaModelConfig(
|
|
451
|
+
fields=["id", "name", "books"]
|
|
452
|
+
)
|
|
453
|
+
relations_as_id = ["books"] # Serialize reverse FK as list of IDs
|
|
454
|
+
|
|
455
|
+
class BookSerializer(serializers.Serializer):
|
|
456
|
+
class Meta:
|
|
457
|
+
model = models.Book
|
|
458
|
+
schema_out = serializers.SchemaModelConfig(
|
|
459
|
+
fields=["id", "title", "author"]
|
|
460
|
+
)
|
|
461
|
+
relations_as_id = ["author"] # Serialize forward FK as ID
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Output (Author):**
|
|
465
|
+
|
|
466
|
+
```json
|
|
467
|
+
{
|
|
468
|
+
"id": 1,
|
|
469
|
+
"name": "J.K. Rowling",
|
|
470
|
+
"books": [1, 2, 3]
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**Output (Book):**
|
|
475
|
+
|
|
476
|
+
```json
|
|
477
|
+
{
|
|
478
|
+
"id": 1,
|
|
479
|
+
"title": "Harry Potter",
|
|
480
|
+
"author": 1
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**M2M Example:**
|
|
485
|
+
|
|
486
|
+
```python
|
|
487
|
+
class TagSerializer(serializers.Serializer):
|
|
488
|
+
class Meta:
|
|
489
|
+
model = models.Tag
|
|
490
|
+
schema_out = serializers.SchemaModelConfig(
|
|
491
|
+
fields=["id", "name", "articles"]
|
|
492
|
+
)
|
|
493
|
+
relations_as_id = ["articles"] # Reverse M2M as list of IDs
|
|
494
|
+
|
|
495
|
+
class ArticleSerializer(serializers.Serializer):
|
|
496
|
+
class Meta:
|
|
497
|
+
model = models.Article
|
|
498
|
+
schema_out = serializers.SchemaModelConfig(
|
|
499
|
+
fields=["id", "title", "tags"]
|
|
500
|
+
)
|
|
501
|
+
relations_as_id = ["tags"] # Forward M2M as list of IDs
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Output (Article):**
|
|
505
|
+
|
|
506
|
+
```json
|
|
507
|
+
{
|
|
508
|
+
"id": 1,
|
|
509
|
+
"title": "Getting Started with Django",
|
|
510
|
+
"tags": [1, 2, 5]
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**UUID Primary Key Example:**
|
|
515
|
+
|
|
516
|
+
When related models use UUID primary keys, the output type is automatically `UUID`:
|
|
517
|
+
|
|
518
|
+
```python
|
|
519
|
+
import uuid
|
|
520
|
+
from django.db import models
|
|
521
|
+
|
|
522
|
+
class Author(models.Model):
|
|
523
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
524
|
+
name = models.CharField(max_length=200)
|
|
525
|
+
|
|
526
|
+
class AuthorSerializer(serializers.Serializer):
|
|
527
|
+
class Meta:
|
|
528
|
+
model = Author
|
|
529
|
+
schema_out = serializers.SchemaModelConfig(
|
|
530
|
+
fields=["id", "name", "books"]
|
|
531
|
+
)
|
|
532
|
+
relations_as_id = ["books"]
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Output (Author with UUID):**
|
|
536
|
+
|
|
537
|
+
```json
|
|
538
|
+
{
|
|
539
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
540
|
+
"name": "J.K. Rowling",
|
|
541
|
+
"books": [
|
|
542
|
+
"6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
543
|
+
"6ba7b811-9dad-11d1-80b4-00c04fd430c8"
|
|
544
|
+
]
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Combining with relations_serializers:**
|
|
549
|
+
|
|
550
|
+
You can use both `relations_as_id` and `relations_serializers` in the same serializer. Fields in `relations_as_id` take precedence:
|
|
551
|
+
|
|
552
|
+
```python
|
|
553
|
+
class ArticleSerializer(serializers.Serializer):
|
|
554
|
+
class Meta:
|
|
555
|
+
model = models.Article
|
|
556
|
+
schema_out = serializers.SchemaModelConfig(
|
|
557
|
+
fields=["id", "title", "author", "tags", "category"]
|
|
558
|
+
)
|
|
559
|
+
relations_serializers = {
|
|
560
|
+
"author": AuthorSerializer, # Nested object
|
|
561
|
+
"category": CategorySerializer, # Nested object
|
|
562
|
+
}
|
|
563
|
+
relations_as_id = ["tags"] # Just IDs
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Query Optimization Note:** When using `relations_as_id`, you should still use `select_related()` for forward relations and `prefetch_related()` for reverse/M2M relations to avoid N+1 queries:
|
|
567
|
+
|
|
568
|
+
```python
|
|
569
|
+
class ArticleSerializer(serializers.Serializer):
|
|
570
|
+
class Meta:
|
|
571
|
+
model = models.Article
|
|
572
|
+
schema_out = serializers.SchemaModelConfig(
|
|
573
|
+
fields=["id", "title", "author", "tags"]
|
|
574
|
+
)
|
|
575
|
+
relations_as_id = ["author", "tags"]
|
|
576
|
+
|
|
577
|
+
class QuerySet:
|
|
578
|
+
read = ModelQuerySetSchema(
|
|
579
|
+
select_related=["author"], # For forward FK
|
|
580
|
+
prefetch_related=["tags"], # For M2M
|
|
581
|
+
)
|
|
582
|
+
|
|
418
583
|
## Using with APIViewSet
|
|
419
584
|
|
|
420
585
|
You can attach a Serializer to an APIViewSet to auto-generate schemas and leverage queryset_request when present:
|
{django_ninja_aio_crud-2.13.0 → django_ninja_aio_crud-2.15.0}/ninja_aio/models/serializers.py
RENAMED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import (
|
|
2
|
+
Annotated,
|
|
3
|
+
Any,
|
|
4
|
+
List,
|
|
5
|
+
Literal,
|
|
6
|
+
Optional,
|
|
7
|
+
Union,
|
|
8
|
+
get_args,
|
|
9
|
+
get_origin,
|
|
10
|
+
ForwardRef,
|
|
11
|
+
)
|
|
2
12
|
import warnings
|
|
3
13
|
import sys
|
|
4
14
|
|
|
@@ -14,6 +24,7 @@ from django.db.models.fields.related_descriptors import (
|
|
|
14
24
|
ForwardManyToOneDescriptor,
|
|
15
25
|
ForwardOneToOneDescriptor,
|
|
16
26
|
)
|
|
27
|
+
from pydantic import BeforeValidator, Field
|
|
17
28
|
|
|
18
29
|
from ninja_aio.types import (
|
|
19
30
|
S_TYPES,
|
|
@@ -28,6 +39,32 @@ from ninja_aio.schemas.helpers import (
|
|
|
28
39
|
)
|
|
29
40
|
|
|
30
41
|
|
|
42
|
+
def _extract_pk(v: Any) -> Any:
|
|
43
|
+
"""Extract primary key from a model instance or return value as-is."""
|
|
44
|
+
if hasattr(v, "pk"):
|
|
45
|
+
return v.pk
|
|
46
|
+
return v
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PkFromModel:
|
|
50
|
+
"""Subscriptable type for extracting PK from model instances during serialization.
|
|
51
|
+
|
|
52
|
+
Usage:
|
|
53
|
+
PkFromModel[int] -> for integer PKs
|
|
54
|
+
PkFromModel[str] -> for string PKs
|
|
55
|
+
PkFromModel[UUID] -> for UUID PKs
|
|
56
|
+
PkFromModel -> defaults to int (backwards compatible)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
_default = Annotated[int, BeforeValidator(_extract_pk)]
|
|
60
|
+
|
|
61
|
+
def __class_getitem__(cls, pk_type: type) -> type:
|
|
62
|
+
return Annotated[pk_type, BeforeValidator(_extract_pk)]
|
|
63
|
+
|
|
64
|
+
def __new__(cls):
|
|
65
|
+
return cls._default
|
|
66
|
+
|
|
67
|
+
|
|
31
68
|
class BaseSerializer:
|
|
32
69
|
"""
|
|
33
70
|
BaseSerializer
|
|
@@ -212,6 +249,11 @@ class BaseSerializer:
|
|
|
212
249
|
# Optional in subclasses. Default to no explicit relation serializers.
|
|
213
250
|
return {}
|
|
214
251
|
|
|
252
|
+
@classmethod
|
|
253
|
+
def _get_relations_as_id(cls) -> list[str]:
|
|
254
|
+
# Optional in subclasses. Default to no relations as ID.
|
|
255
|
+
return []
|
|
256
|
+
|
|
215
257
|
@classmethod
|
|
216
258
|
def _generate_union_schema(cls, resolved_union: Any) -> Any:
|
|
217
259
|
"""
|
|
@@ -350,10 +392,25 @@ class BaseSerializer:
|
|
|
350
392
|
) or cls._is_special_field("update", field, "optionals")
|
|
351
393
|
|
|
352
394
|
@classmethod
|
|
353
|
-
def _build_schema_reverse_rel(
|
|
395
|
+
def _build_schema_reverse_rel(
|
|
396
|
+
cls, field_name: str, descriptor: Any, relations_as_id: list[str]
|
|
397
|
+
):
|
|
354
398
|
"""
|
|
355
399
|
Build a reverse relation schema component for 'Out' schema generation.
|
|
356
|
-
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
field_name : str
|
|
404
|
+
Name of the relation field.
|
|
405
|
+
descriptor : Any
|
|
406
|
+
Django field descriptor (ManyToManyDescriptor, ReverseManyToOneDescriptor, etc.).
|
|
407
|
+
relations_as_id : list[str]
|
|
408
|
+
Pre-fetched list of fields to serialize as IDs.
|
|
409
|
+
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
tuple | None
|
|
413
|
+
Custom field tuple for schema generation, or None to skip.
|
|
357
414
|
"""
|
|
358
415
|
# Resolve related model and cardinality
|
|
359
416
|
if isinstance(descriptor, ManyToManyDescriptor):
|
|
@@ -371,6 +428,17 @@ class BaseSerializer:
|
|
|
371
428
|
rel_model = descriptor.related.related_model
|
|
372
429
|
many = False
|
|
373
430
|
|
|
431
|
+
# Handle relations_as_id for reverse relations
|
|
432
|
+
if field_name in relations_as_id:
|
|
433
|
+
from ninja_aio.models.utils import ModelUtil
|
|
434
|
+
pk_field_type = ModelUtil(rel_model).pk_field_type
|
|
435
|
+
if many:
|
|
436
|
+
# For many relations, use PkFromModel to extract PKs from model instances
|
|
437
|
+
return (field_name, list[PkFromModel[pk_field_type]], Field(default_factory=list))
|
|
438
|
+
else:
|
|
439
|
+
# For single reverse relations (ReverseOneToOne), extract pk
|
|
440
|
+
return (field_name, PkFromModel[pk_field_type] | None, None)
|
|
441
|
+
|
|
374
442
|
schema = cls._resolve_relation_schema(field_name, rel_model)
|
|
375
443
|
if not schema:
|
|
376
444
|
return None
|
|
@@ -379,14 +447,36 @@ class BaseSerializer:
|
|
|
379
447
|
return (field_name, rel_schema_type | None, None)
|
|
380
448
|
|
|
381
449
|
@classmethod
|
|
382
|
-
def _build_schema_forward_rel(
|
|
450
|
+
def _build_schema_forward_rel(
|
|
451
|
+
cls, field_name: str, descriptor: Any, relations_as_id: list[str]
|
|
452
|
+
):
|
|
383
453
|
"""
|
|
384
454
|
Build a forward relation schema component for 'Out' schema generation.
|
|
385
|
-
|
|
386
|
-
|
|
455
|
+
|
|
456
|
+
Parameters
|
|
457
|
+
----------
|
|
458
|
+
field_name : str
|
|
459
|
+
Name of the relation field.
|
|
460
|
+
descriptor : Any
|
|
461
|
+
Django field descriptor (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor).
|
|
462
|
+
relations_as_id : list[str]
|
|
463
|
+
Pre-fetched list of fields to serialize as IDs.
|
|
464
|
+
|
|
465
|
+
Returns
|
|
466
|
+
-------
|
|
467
|
+
True | tuple | None
|
|
468
|
+
True to treat as plain field, a custom field tuple to include relation schema,
|
|
469
|
+
or None to skip entirely.
|
|
387
470
|
"""
|
|
388
471
|
rel_model = descriptor.field.related_model
|
|
389
472
|
|
|
473
|
+
# Handle relations_as_id: serialize as the raw FK ID
|
|
474
|
+
if field_name in relations_as_id:
|
|
475
|
+
from ninja_aio.models.utils import ModelUtil
|
|
476
|
+
pk_field_type = ModelUtil(rel_model).pk_field_type
|
|
477
|
+
# Use PkFromModel to extract pk from the related instance during serialization
|
|
478
|
+
return (field_name, PkFromModel[pk_field_type] | None, None)
|
|
479
|
+
|
|
390
480
|
# Special case: ModelSerializer with no readable fields should be skipped entirely
|
|
391
481
|
if isinstance(rel_model, ModelSerializerMeta):
|
|
392
482
|
if not (
|
|
@@ -441,23 +531,44 @@ class BaseSerializer:
|
|
|
441
531
|
field_name: str,
|
|
442
532
|
model,
|
|
443
533
|
relations_serializers: dict,
|
|
534
|
+
relations_as_id: list[str],
|
|
444
535
|
) -> tuple[str | None, tuple | None, tuple | None]:
|
|
445
536
|
"""
|
|
446
537
|
Process a single field and determine its classification.
|
|
447
538
|
|
|
448
|
-
|
|
539
|
+
Parameters
|
|
540
|
+
----------
|
|
541
|
+
field_name : str
|
|
542
|
+
Name of the field to process.
|
|
543
|
+
model : Model
|
|
544
|
+
Django model class.
|
|
545
|
+
relations_serializers : dict
|
|
546
|
+
Mapping of relation field names to serializer classes.
|
|
547
|
+
relations_as_id : list[str]
|
|
548
|
+
Pre-fetched list of fields to serialize as IDs.
|
|
549
|
+
|
|
550
|
+
Returns
|
|
551
|
+
-------
|
|
552
|
+
tuple
|
|
449
553
|
(plain_field, reverse_rel, forward_rel) - only one will be non-None
|
|
450
554
|
"""
|
|
451
555
|
field_obj = getattr(model, field_name)
|
|
452
556
|
|
|
453
557
|
if cls._is_reverse_relation(field_obj):
|
|
454
|
-
if
|
|
558
|
+
if (
|
|
559
|
+
field_name not in relations_serializers
|
|
560
|
+
and field_name not in relations_as_id
|
|
561
|
+
):
|
|
455
562
|
cls._warn_missing_relation_serializer(field_name, model)
|
|
456
|
-
rel_tuple = cls._build_schema_reverse_rel(
|
|
563
|
+
rel_tuple = cls._build_schema_reverse_rel(
|
|
564
|
+
field_name, field_obj, relations_as_id
|
|
565
|
+
)
|
|
457
566
|
return (None, rel_tuple, None)
|
|
458
567
|
|
|
459
568
|
if cls._is_forward_relation(field_obj):
|
|
460
|
-
rel_tuple = cls._build_schema_forward_rel(
|
|
569
|
+
rel_tuple = cls._build_schema_forward_rel(
|
|
570
|
+
field_name, field_obj, relations_as_id
|
|
571
|
+
)
|
|
461
572
|
if rel_tuple is True:
|
|
462
573
|
return (field_name, None, None)
|
|
463
574
|
return (None, None, rel_tuple)
|
|
@@ -469,8 +580,15 @@ class BaseSerializer:
|
|
|
469
580
|
"""
|
|
470
581
|
Collect components for output schema generation (Out or Detail).
|
|
471
582
|
|
|
472
|
-
|
|
473
|
-
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
schema_type : Literal["Out", "Detail"]
|
|
586
|
+
Type of schema to generate.
|
|
587
|
+
|
|
588
|
+
Returns
|
|
589
|
+
-------
|
|
590
|
+
tuple
|
|
591
|
+
(fields, reverse_rels, excludes, customs_with_forward_rels, optionals)
|
|
474
592
|
"""
|
|
475
593
|
if schema_type not in ("Out", "Detail"):
|
|
476
594
|
raise ValueError(
|
|
@@ -480,6 +598,8 @@ class BaseSerializer:
|
|
|
480
598
|
fields_type = "read" if schema_type == "Out" else "detail"
|
|
481
599
|
model = cls._get_model()
|
|
482
600
|
relations_serializers = cls._get_relations_serializers() or {}
|
|
601
|
+
# Fetch once to avoid repeated method calls during field processing
|
|
602
|
+
relations_as_id = cls._get_relations_as_id()
|
|
483
603
|
|
|
484
604
|
fields: list[str] = []
|
|
485
605
|
reverse_rels: list[tuple] = []
|
|
@@ -487,7 +607,7 @@ class BaseSerializer:
|
|
|
487
607
|
|
|
488
608
|
for field_name in cls.get_fields(fields_type):
|
|
489
609
|
plain, reverse, forward = cls._process_field(
|
|
490
|
-
field_name, model, relations_serializers
|
|
610
|
+
field_name, model, relations_serializers, relations_as_id
|
|
491
611
|
)
|
|
492
612
|
if plain:
|
|
493
613
|
fields.append(plain)
|
|
@@ -701,12 +821,15 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
701
821
|
Computed / synthetic output attributes.
|
|
702
822
|
optionals : list[tuple[str, type]]
|
|
703
823
|
Optional output fields.
|
|
824
|
+
relations_as_id : list[str]
|
|
825
|
+
Relation fields to serialize as IDs instead of nested objects.
|
|
704
826
|
"""
|
|
705
827
|
|
|
706
828
|
fields: list[str] = []
|
|
707
829
|
customs: list[tuple[str, type, Any]] = []
|
|
708
830
|
optionals: list[tuple[str, type]] = []
|
|
709
831
|
excludes: list[str] = []
|
|
832
|
+
relations_as_id: list[str] = []
|
|
710
833
|
|
|
711
834
|
class DetailSerializer:
|
|
712
835
|
"""Configuration describing detail (single object) read schema.
|
|
@@ -756,6 +879,11 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
756
879
|
"detail": "DetailSerializer",
|
|
757
880
|
}
|
|
758
881
|
|
|
882
|
+
@classmethod
|
|
883
|
+
def _get_relations_as_id(cls) -> list[str]:
|
|
884
|
+
"""Return relation fields to serialize as IDs instead of nested objects."""
|
|
885
|
+
return getattr(cls.ReadSerializer, "relations_as_id", [])
|
|
886
|
+
|
|
759
887
|
@classmethod
|
|
760
888
|
def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
|
|
761
889
|
"""
|
|
@@ -960,6 +1088,12 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
960
1088
|
schema_update: Optional[SchemaModelConfig] = None
|
|
961
1089
|
schema_detail: Optional[SchemaModelConfig] = None
|
|
962
1090
|
relations_serializers: dict[str, "Serializer"] = {}
|
|
1091
|
+
relations_as_id: list[str] = []
|
|
1092
|
+
|
|
1093
|
+
@classmethod
|
|
1094
|
+
def _get_relations_as_id(cls) -> list[str]:
|
|
1095
|
+
relations_as_id = cls._get_meta_data("relations_as_id")
|
|
1096
|
+
return relations_as_id or []
|
|
963
1097
|
|
|
964
1098
|
@classmethod
|
|
965
1099
|
def _get_meta_data(cls, attr_name: str) -> Any:
|