# saritasa-drf-tools

[](https://pypi.org/project/saritasa-drf-tools/)





[](https://github.com/astral-sh/ruff)
Tools For [DRF](https://www.django-rest-framework.org/) Used By Saritasa
## Table of contents
* [Installation](#installation)
* [Features](#features)
* [Optional dependencies](#optional-dependencies)
* [Serializers](#serializers)
* [Views](#views)
* [Pagination](#pagination)
* [Filters](#filters)
* [Renderers](#renderers)
* [OpenAPI](#openapi)
* [Tester](#tester)
## Installation
```bash
pip install saritasa-drf-tools
```
or if you are using [uv](https://docs.astral.sh/uv/)
```bash
uv add saritasa-drf-tools
```
or if you are using [poetry](https://python-poetry.org/)
```bash
poetry add saritasa-drf-tools
```
## Features
* Views - collection of mixins and viewsets classes
* Serializers - collection of mixins and serializers classes
* Filters - Custom filter backends that improve integration with
[drf-spectacular](https://github.com/tfranzel/drf-spectacular)
* OpenAPI - tools for [drf-spectacular](https://github.com/tfranzel/drf-spectacular)
* `pytest` - plugin which provides different `api_client` fixtures.
* Testing classes(**Warning: Very experimental**) - Test class which contains shortcut to reduce boilerplate across tests.
For examples and to just check it out in action you can use [example folder](/example).
## Optional dependencies
* `[filters]` - Add this to enable `django-filters` support
* `[openapi]` - Add this to enable `drf-spectacular` support
## Views
### Views mixins
* `ActionPermissionsMixin`: Mixin which allows to define specific permissions per actions
For example you have:
```python
class CRUDView(
ActionPermissionsMixin,
ActionMixins, # Anything you need
GenericViewSet,
):
"""CRUD view."""
base_permission_classes = (permissions.AllowAny,)
extra_permission_classes = (permissions.IsAuthenticated,)
extra_permissions_map = {
"create": (permissions.IsAdminUser,),
"update": (permissions.IsAdminUser,),
"destroy": (permissions.IsAdminUser,),
}
```
* `base_permission_classes` - Will be applied to any action (Usually you want this in base class of your project)
* `extra_permission_classes` - Will be added to `base_permission_classes`
* `extra_permission_map` - Will be added to (`base_permission_classes` + `extra_permission_classes`) on
action you specify in mapping
To learn more read class docs.
* `ActionSerializerMixin`: Mixin which allows to define specific serializers per action.
For example you have
```python
class CRUDView(
ActionPermissionsMixin,
ActionMixins, # Anything you need
GenericViewSet,
):
"""CRUD view."""
queryset = models.TestModel.objects.select_related("related_model").all()
serializers_map = {
"default": serializers.TestModelDetailSerializer,
"list": serializers.TestModelListSerializer,
}
```
That means that on `list` view will use `TestModelListSerializer`, but on any other
actions `TestModelDetailSerializer`. This will also will be reflected in
generated openapi specs via [drf-spectacular](https://github.com/tfranzel/drf-spectacular)
To learn more read class docs.
* `UpdateModelWithoutPatchMixin`: Same as UpdateModelMixin but without patch method
### Viewset classes
* `BaseViewSet`: Viewset with `ActionPermissionsMixin` and `ActionSerializerMixin`
* `CRUDViewSet`: Viewset with crud endpoint based on BaseViewSet
* `ReadOnlyViewSet`: Viewset with read endpoint based on BaseViewSet
## Pagination
* `LimitOffsetPagination`: Customized paginator class to limit max objects in list APIs.
Use `SARITASA_DRF_MAX_PAGINATION_SIZE` to set default max for whole project.
## Serializers
### Serializers mixins
* `CleanValidationMixin`: Enable model `clean` validation in serializer
* `FieldMappingOverride`: Override or extend field mapping via `SARITASA_DRF_FIELD_MAPPING`.
For example you can set following in settings.
```python
SARITASA_DRF_FIELD_MAPPING = {
"django.db.models.TextField": "example.app.api.fields.CustomCharField",
}
```
And now all `TextField` of your models will have `CustomCharField` in
serializers.
* `UserAndRequestFromContextMixin`: Extracts user and request from context
and sets it as attr of serializer instance.
* `NestedFieldsMixin`: Allows to define nested data fields for serializers via `Meta` class.
### Serializers classes
* `BaseSerializer`: Serializer with `UserAndRequestFromContextMixin`
* `ModelBaseSerializer`: ModelSerializer with `mixins.FieldMappingOverride`,
`mixins.CleanValidationMixin`, `mixins.UserAndRequestFromContextMixin`,
`mixins.NestedFieldsMixin`.
## Filters
Needs `filters` and `openapi` to be included to work properly.
* `OrderingFilterBackend`: Add supported fields to `ordering` param's description
in specs generated by [drf-spectacular](https://github.com/tfranzel/drf-spectacular). Will raise warning specs validation
on empty `ordering_fields` or if queryset is unable to order itself using `ordering_fields`.
Example of description:
```text
Which fields to use when ordering the results. A list fields separated by ,. Example: field1,field2
Supported fields: id, text_field, related_model__text_field.
To reverse order just add - to field. Example:field -> -field
```
Also has support for `nulls_first` and `nulls_last` ordering.
You can either set these options globally in your settings:
```python
SARITASA_DRF_ORDERING_IS_NULL_FIRST = True
SARITASA_DRF_ORDERING_IS_NULL_LAST = True
```
Or you can set them per view:
```python
class MyView(views.APIView):
ordering_fields_extra_kwargs = {
"my_field": {
"nulls_first": True,
"nulls_last": False,
},
}
```
* `SearchFilterBackend`: Add supported fields to `search` param's description
in specs generated by [drf-spectacular](https://github.com/tfranzel/drf-spectacular). Will raise warning specs validation
on empty `search_fields` or if queryset is unable to perform search using `search_fields`.
Example of description:
```text
A search term.
Performed on this fields: text_field, related_model__text_field.
```
* `DjangoFilterBackend`: Customized `DjangoFilterBackend` to reduce queries count when viewing api requests via browser
## Renderers
* `BrowsableAPIRenderer`: Customization over drf's BrowsableAPIRenderer.
With `SARITASA_DRF_BROWSABLE_API_ENABLE_HTML_FORM`(Default: `True`) or
setting `enable_browsable_api_rendered_html_form`(If not present will use global setting)
in view you can disable all extra forms which results in extra SQL queries.
## OpenAPI
Needs `openapi` to be included to work properly.
* `OpenApiSerializer`: Serializer that should be used for customizing open_api spec.
Made to avoid warnings about unimplemented methods.
* `DetailSerializer`: To show in spec responses like this `{detail: text}`.
* `fix_api_view_warning`: Fix warning `This is graceful fallback handling for APIViews`.
## Pytest
Plugin provides following fixtures:
* `api_client_factory` - factory which generated `rest_framework.test.ApiClient` instance
* `api_client` - uses `api_client_factory` to generate `rest_framework.test.ApiClient` instance
* `user_api_client`(Needs `user` fixture) uses `api_client_factory` to generate `rest_framework.test.ApiClient` instance
forces auth to `user`
* `admin_api_client`(Needs `admin` fixture) uses `api_client_factory` to generate `rest_framework.test.ApiClient` instance
forces auth to `admin`
## Tester
**Warning**: Very experimental.
`saritasa_drf_tools.testing.ApiActionTester` - is a tester class which contains
fixtures and shortcuts to simply and reduce boilerplate in tests for viewsets.
All you need to is create `tester.py`(you what ever you want it's just recommendation).
In this file declare new class which inherits `ApiActionTester`.
```python
class CRUDApiActionTester(
saritasa_drf_tools.testing.ApiActionTester.init_subclass(
model=models.TestModel, # Model of queryset in viewset
user_model=models.User, # Model of user used across project
factory=factories.TestModelFactory, # Factory which is used to generate instances for model
api_view=api.views.CRUDView, # Class of viewset against which we will be writing tests
url_basename="crud-api", # Base name of urls of viewset. {url_basename}-{action}
),
):
"""Tester for crud API."""
```
Next you can write test just like this. (For more examples check this [folder](tests/test_crud_api))
```python
class TestCRUD(tester.CRUDApiActionTester):
"""Define tests."""
@pytest.mark.parametrize(
argnames=[
"parametrize_user",
"status_code",
],
argvalues=[
[
None,
status.HTTP_403_FORBIDDEN,
],
[
pytest_lazy_fixtures.lf("user"),
status.HTTP_403_FORBIDDEN,
],
[
pytest_lazy_fixtures.lf("admin"),
status.HTTP_201_CREATED,
],
],
)
def test_permission_map_specified_action_create(
self,
instance: tester.CRUDApiActionTester.model,
parametrize_user: tester.CRUDApiActionTester.user_model | None,
status_code: int,
) -> None:
"""Test that create action will properly handle permissions."""
self.make_request(
method="post",
user=parametrize_user,
expected_status=status_code,
path=self.lazy_url(action="list"),
data=self.serialize_data(action="create", data=instance),
)
```
Raw data
{
"_id": null,
"home_page": "https://pypi.org/project/saritasa-drf-tools/",
"name": "saritasa-drf-tools",
"maintainer": "Stanislav Khlud",
"docs_url": null,
"requires_python": "<4.0,>=3.12",
"maintainer_email": "stanislav.khlud@saritasa.com",
"keywords": "python, django, drf",
"author": "Saritasa",
"author_email": "pypi@saritasa.com",
"download_url": "https://files.pythonhosted.org/packages/7e/1b/8fa1c724b63454688dfb6484937c15f1b190a45713a31588dc3f89c206b9/saritasa_drf_tools-0.2.0.tar.gz",
"platform": null,
"description": "# saritasa-drf-tools\n\n\n[](https://pypi.org/project/saritasa-drf-tools/)\n\n\n\n\n\n[](https://github.com/astral-sh/ruff)\n\nTools For [DRF](https://www.django-rest-framework.org/) Used By Saritasa\n\n## Table of contents\n\n* [Installation](#installation)\n* [Features](#features)\n* [Optional dependencies](#optional-dependencies)\n* [Serializers](#serializers)\n* [Views](#views)\n* [Pagination](#pagination)\n* [Filters](#filters)\n* [Renderers](#renderers)\n* [OpenAPI](#openapi)\n* [Tester](#tester)\n\n## Installation\n\n```bash\npip install saritasa-drf-tools\n```\n\nor if you are using [uv](https://docs.astral.sh/uv/)\n\n```bash\nuv add saritasa-drf-tools\n```\n\nor if you are using [poetry](https://python-poetry.org/)\n\n```bash\npoetry add saritasa-drf-tools\n```\n\n## Features\n\n* Views - collection of mixins and viewsets classes\n* Serializers - collection of mixins and serializers classes\n* Filters - Custom filter backends that improve integration with\n [drf-spectacular](https://github.com/tfranzel/drf-spectacular)\n* OpenAPI - tools for [drf-spectacular](https://github.com/tfranzel/drf-spectacular)\n* `pytest` - plugin which provides different `api_client` fixtures.\n* Testing classes(**Warning: Very experimental**) - Test class which contains shortcut to reduce boilerplate across tests.\n\nFor examples and to just check it out in action you can use [example folder](/example).\n\n## Optional dependencies\n\n* `[filters]` - Add this to enable `django-filters` support\n* `[openapi]` - Add this to enable `drf-spectacular` support\n\n## Views\n\n### Views mixins\n\n* `ActionPermissionsMixin`: Mixin which allows to define specific permissions per actions\n For example you have:\n\n ```python\n class CRUDView(\n ActionPermissionsMixin,\n ActionMixins, # Anything you need\n GenericViewSet,\n ):\n \"\"\"CRUD view.\"\"\"\n base_permission_classes = (permissions.AllowAny,)\n extra_permission_classes = (permissions.IsAuthenticated,)\n extra_permissions_map = {\n \"create\": (permissions.IsAdminUser,),\n \"update\": (permissions.IsAdminUser,),\n \"destroy\": (permissions.IsAdminUser,),\n }\n ```\n\n * `base_permission_classes` - Will be applied to any action (Usually you want this in base class of your project)\n * `extra_permission_classes` - Will be added to `base_permission_classes`\n * `extra_permission_map` - Will be added to (`base_permission_classes` + `extra_permission_classes`) on\n action you specify in mapping\n\n To learn more read class docs.\n\n* `ActionSerializerMixin`: Mixin which allows to define specific serializers per action.\n For example you have\n\n ```python\n class CRUDView(\n ActionPermissionsMixin,\n ActionMixins, # Anything you need\n GenericViewSet,\n ):\n \"\"\"CRUD view.\"\"\"\n\n queryset = models.TestModel.objects.select_related(\"related_model\").all()\n serializers_map = {\n \"default\": serializers.TestModelDetailSerializer,\n \"list\": serializers.TestModelListSerializer,\n }\n ```\n\n That means that on `list` view will use `TestModelListSerializer`, but on any other\n actions `TestModelDetailSerializer`. This will also will be reflected in\n generated openapi specs via [drf-spectacular](https://github.com/tfranzel/drf-spectacular)\n\n To learn more read class docs.\n\n* `UpdateModelWithoutPatchMixin`: Same as UpdateModelMixin but without patch method\n\n### Viewset classes\n\n* `BaseViewSet`: Viewset with `ActionPermissionsMixin` and `ActionSerializerMixin`\n* `CRUDViewSet`: Viewset with crud endpoint based on BaseViewSet\n* `ReadOnlyViewSet`: Viewset with read endpoint based on BaseViewSet\n\n## Pagination\n\n* `LimitOffsetPagination`: Customized paginator class to limit max objects in list APIs.\n Use `SARITASA_DRF_MAX_PAGINATION_SIZE` to set default max for whole project.\n\n## Serializers\n\n### Serializers mixins\n\n* `CleanValidationMixin`: Enable model `clean` validation in serializer\n* `FieldMappingOverride`: Override or extend field mapping via `SARITASA_DRF_FIELD_MAPPING`.\n For example you can set following in settings.\n\n ```python\n SARITASA_DRF_FIELD_MAPPING = {\n \"django.db.models.TextField\": \"example.app.api.fields.CustomCharField\",\n }\n ```\n\n And now all `TextField` of your models will have `CustomCharField` in\n serializers.\n\n* `UserAndRequestFromContextMixin`: Extracts user and request from context\n and sets it as attr of serializer instance.\n* `NestedFieldsMixin`: Allows to define nested data fields for serializers via `Meta` class.\n\n### Serializers classes\n\n* `BaseSerializer`: Serializer with `UserAndRequestFromContextMixin`\n* `ModelBaseSerializer`: ModelSerializer with `mixins.FieldMappingOverride`,\n `mixins.CleanValidationMixin`, `mixins.UserAndRequestFromContextMixin`,\n `mixins.NestedFieldsMixin`.\n\n## Filters\n\nNeeds `filters` and `openapi` to be included to work properly.\n\n* `OrderingFilterBackend`: Add supported fields to `ordering` param's description\n in specs generated by [drf-spectacular](https://github.com/tfranzel/drf-spectacular). Will raise warning specs validation\n on empty `ordering_fields` or if queryset is unable to order itself using `ordering_fields`.\n Example of description:\n\n ```text\n Which fields to use when ordering the results. A list fields separated by ,. Example: field1,field2\n\n Supported fields: id, text_field, related_model__text_field.\n\n To reverse order just add - to field. Example:field -> -field\n ```\n\n Also has support for `nulls_first` and `nulls_last` ordering.\n You can either set these options globally in your settings:\n\n ```python\n SARITASA_DRF_ORDERING_IS_NULL_FIRST = True\n SARITASA_DRF_ORDERING_IS_NULL_LAST = True\n ```\n\n Or you can set them per view:\n\n ```python\n class MyView(views.APIView):\n ordering_fields_extra_kwargs = {\n \"my_field\": {\n \"nulls_first\": True,\n \"nulls_last\": False,\n },\n }\n ```\n\n* `SearchFilterBackend`: Add supported fields to `search` param's description\n in specs generated by [drf-spectacular](https://github.com/tfranzel/drf-spectacular). Will raise warning specs validation\n on empty `search_fields` or if queryset is unable to perform search using `search_fields`.\n\n Example of description:\n\n ```text\n A search term.\n\n Performed on this fields: text_field, related_model__text_field.\n ```\n\n* `DjangoFilterBackend`: Customized `DjangoFilterBackend` to reduce queries count when viewing api requests via browser\n\n## Renderers\n\n* `BrowsableAPIRenderer`: Customization over drf's BrowsableAPIRenderer.\n With `SARITASA_DRF_BROWSABLE_API_ENABLE_HTML_FORM`(Default: `True`) or\n setting `enable_browsable_api_rendered_html_form`(If not present will use global setting)\n in view you can disable all extra forms which results in extra SQL queries.\n\n## OpenAPI\n\nNeeds `openapi` to be included to work properly.\n\n* `OpenApiSerializer`: Serializer that should be used for customizing open_api spec.\n Made to avoid warnings about unimplemented methods.\n* `DetailSerializer`: To show in spec responses like this `{detail: text}`.\n* `fix_api_view_warning`: Fix warning `This is graceful fallback handling for APIViews`.\n\n## Pytest\n\nPlugin provides following fixtures:\n\n* `api_client_factory` - factory which generated `rest_framework.test.ApiClient` instance\n* `api_client` - uses `api_client_factory` to generate `rest_framework.test.ApiClient` instance\n* `user_api_client`(Needs `user` fixture) uses `api_client_factory` to generate `rest_framework.test.ApiClient` instance\n forces auth to `user`\n* `admin_api_client`(Needs `admin` fixture) uses `api_client_factory` to generate `rest_framework.test.ApiClient` instance\n forces auth to `admin`\n\n## Tester\n\n**Warning**: Very experimental.\n\n`saritasa_drf_tools.testing.ApiActionTester` - is a tester class which contains\nfixtures and shortcuts to simply and reduce boilerplate in tests for viewsets.\n\nAll you need to is create `tester.py`(you what ever you want it's just recommendation).\nIn this file declare new class which inherits `ApiActionTester`.\n\n```python\nclass CRUDApiActionTester(\n saritasa_drf_tools.testing.ApiActionTester.init_subclass(\n model=models.TestModel, # Model of queryset in viewset\n user_model=models.User, # Model of user used across project\n factory=factories.TestModelFactory, # Factory which is used to generate instances for model\n api_view=api.views.CRUDView, # Class of viewset against which we will be writing tests\n url_basename=\"crud-api\", # Base name of urls of viewset. {url_basename}-{action}\n ),\n):\n \"\"\"Tester for crud API.\"\"\"\n```\n\nNext you can write test just like this. (For more examples check this [folder](tests/test_crud_api))\n\n```python\nclass TestCRUD(tester.CRUDApiActionTester):\n \"\"\"Define tests.\"\"\"\n\n @pytest.mark.parametrize(\n argnames=[\n \"parametrize_user\",\n \"status_code\",\n ],\n argvalues=[\n [\n None,\n status.HTTP_403_FORBIDDEN,\n ],\n [\n pytest_lazy_fixtures.lf(\"user\"),\n status.HTTP_403_FORBIDDEN,\n ],\n [\n pytest_lazy_fixtures.lf(\"admin\"),\n status.HTTP_201_CREATED,\n ],\n ],\n )\n def test_permission_map_specified_action_create(\n self,\n instance: tester.CRUDApiActionTester.model,\n parametrize_user: tester.CRUDApiActionTester.user_model | None,\n status_code: int,\n ) -> None:\n \"\"\"Test that create action will properly handle permissions.\"\"\"\n self.make_request(\n method=\"post\",\n user=parametrize_user,\n expected_status=status_code,\n path=self.lazy_url(action=\"list\"),\n data=self.serialize_data(action=\"create\", data=instance),\n )\n```\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Tools For DRF Used By Saritasa",
"version": "0.2.0",
"project_urls": {
"Homepage": "https://pypi.org/project/saritasa-drf-tools/",
"Repository": "https://github.com/saritasa-nest/saritasa-drf-tools/"
},
"split_keywords": [
"python",
" django",
" drf"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "0e6beecbd4c111eba3b4a6f3afa5a53177dbcb0e703b4948adc1c6841fa993d9",
"md5": "22f1f493843d3127ab55e656e8f8759e",
"sha256": "2bffcc15345ed32a66b7036a2326edc47418f83fcb8214d682b0974019285f70"
},
"downloads": -1,
"filename": "saritasa_drf_tools-0.2.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "22f1f493843d3127ab55e656e8f8759e",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.12",
"size": 26046,
"upload_time": "2025-09-08T08:43:42",
"upload_time_iso_8601": "2025-09-08T08:43:42.812111Z",
"url": "https://files.pythonhosted.org/packages/0e/6b/eecbd4c111eba3b4a6f3afa5a53177dbcb0e703b4948adc1c6841fa993d9/saritasa_drf_tools-0.2.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "7e1b8fa1c724b63454688dfb6484937c15f1b190a45713a31588dc3f89c206b9",
"md5": "4b0c73f1e2e74db5548f5caa1450c1a0",
"sha256": "a48479e69a9a515eb5b92af87ee96a72200bcd523da7760b63bb3cb4b7ed6ef8"
},
"downloads": -1,
"filename": "saritasa_drf_tools-0.2.0.tar.gz",
"has_sig": false,
"md5_digest": "4b0c73f1e2e74db5548f5caa1450c1a0",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.12",
"size": 20887,
"upload_time": "2025-09-08T08:43:44",
"upload_time_iso_8601": "2025-09-08T08:43:44.049669Z",
"url": "https://files.pythonhosted.org/packages/7e/1b/8fa1c724b63454688dfb6484937c15f1b190a45713a31588dc3f89c206b9/saritasa_drf_tools-0.2.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-09-08 08:43:44",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "saritasa-nest",
"github_project": "saritasa-drf-tools",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "saritasa-drf-tools"
}