Metadata-Version: 2.4
Name: saritasa-drf-tools
Version: 0.4.0
Summary: Tools For DRF Used By Saritasa
Project-URL: Documentation, https://saritasa-nest.github.io/saritasa-drf-tools/latest/
Project-URL: Repository, https://github.com/saritasa-nest/saritasa-drf-tools
Project-URL: Changelog, https://saritasa-nest.github.io/saritasa-drf-tools/latest/changelog/
Project-URL: Releases, https://github.com/saritasa-nest/saritasa-drf-tools/releases/
Project-URL: Bug Tracker, https://github.com/saritasa-nest/saritasa-drf-tools/issues/
Project-URL: Contributing, https://saritasa-nest.github.io/saritasa-drf-tools/latest/contributing/
Author-email: Saritasa <pypi@saritasa.com>
Maintainer-email: Stanislav Khlud <stanislav.khlud@saritasa.com>
License-Expression: MIT
License-File: LICENSE
Keywords: django,drf,python
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Requires-Dist: django>=4
Requires-Dist: djangorestframework>=3.15
Provides-Extra: all
Requires-Dist: django-filter>=24; extra == 'all'
Requires-Dist: drf-spectacular>=0.29; extra == 'all'
Provides-Extra: filter
Requires-Dist: django-filter>=24; extra == 'filter'
Provides-Extra: openapi
Requires-Dist: drf-spectacular>=0.29; extra == 'openapi'
Description-Content-Type: text/markdown

# saritasa-drf-tools

![GitHub last commit](https://img.shields.io/github/last-commit/saritasa-nest/saritasa-drf-tools)
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/saritasa-nest/saritasa-drf-tools/run_pre_commit.yaml)
![PyPI](https://img.shields.io/pypi/v/saritasa-drf-tools)
![PyPI - Status](https://img.shields.io/pypi/status/saritasa-drf-tools)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/saritasa-drf-tools)
![PyPI - Django Version](https://img.shields.io/pypi/frameworkversions/django/saritasa-drf-tools)
![PyPI - License](https://img.shields.io/pypi/l/saritasa-drf-tools)
![PyPI - Downloads](https://img.shields.io/pypi/dm/saritasa-drf-tools)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![prek](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/j178/prek/master/docs/assets/badge-v0.json)](https://github.com/j178/prek)

Check out our other open-source [projects](https://github.com/orgs/saritasa-nest/repositories?q=visibility%3Apublic+archived%3Afalse+fork%3Afalse+language%3APython)

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.

## 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.
  Also supports overriding pagination settings per view via
  `pagination_default_limit` and `pagination_max_limit` attributes.

## 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,
          },
      }
  ```

  If you have list of items ordered by non-unique fields only (or without any ordering), this filter will add secondary
  sorting by `pk` to make sure that order of items will be consistent across requests.
  You can enable secondary sorting via `add_pk_to_ordering` flag in your views:

  ```python
  class MyView(views.APIView):
      add_pk_to_ordering = True
  ```

  By default, this option is enabled. You can either set this option globally in your settings:

  ```python
  SARITASA_DRF_ORDERING_ADD_PK_TO_ORDERING = True
  ```

- `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),
        )
```
