django-awesome-tools


Namedjango-awesome-tools JSON
Version 1.5.7 PyPI version JSON
download
home_page
SummaryAwesome functions and classes for Django and Django Rest Framework
upload_time2023-04-05 11:13:26
maintainer
docs_urlNone
author
requires_python>=3.8
licenseMIT License
keywords django utils serializers generic views viewsets mixins email login model manager custom action shortcut error simple rest framework dj drf admin hash password user filter queryset cache management by auth token by user group key docs documentation spectacular customize swagger openapi
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            <div align="center">
    <img src="https://user-images.githubusercontent.com/87717182/222975347-32f01617-71a4-42f3-9db2-248a52ffe819.png">
</div>
<h1 align="center">Django Awesome Tools</h1>

This package provides useful and powerful functions and classes to be used in [Django](https://www.djangoproject.com/) projects, specially when working with [Django Rest Framework](https://www.django-rest-framework.org/). Below are some further explation about how to use this package and what each module inside it does.

## Table of Contents

- [Installation](#installation)
- [helpers](#helpers)
  - [get\_object\_or\_error](#get_object_or_error)
  - [get\_list\_or\_error](#get_list_or_error)
  - [set\_and\_destroy](#set_and_destroy)
  - [bulk\_get\_or\_create](#bulk_get_or_create)
- [mixins](#mixins)
  - [SerializerByMethodMixin](#serializerbymethodmixin)
  - [SerializerByActionMixin](#serializerbyactionmixin)
  - [SerializerByDetailActionsMixin](#serializerbydetailactionsmixin)
  - [SerializerBySafeActionsMixin](#serializerbysafeactionsmixin)
  - [FilterQuerysetMixin](#filterquerysetmixin)
  - [AttachUserOnCreateMixin](#attachuseroncreatemixin)
  - [AttachUserOnUpdateMixin](#attachuseronupdatemixin)
  - [AttachUserToReqDataMixin](#attachusertoreqdatamixin)
- [managers](#managers)
  - [CustomUserManager](#customusermanager)
- [cache](#cache)
  - [build\_cache\_mixins](#build_cache_mixins)
  - [SetCacheOnListMixin](#setcacheonlistmixin)
  - [EraseCacheOnCreateMixin](#erasecacheoncreatemixin)
  - [EraseCacheOnUpdateMixin](#erasecacheonupdatemixin)
  - [EraseCacheOnDestroyMixin](#erasecacheondestroymixin)
  - [EraseCacheOnDetailMixin](#erasecacheondetailmixin)
  - [EraseCacheOnDetailMixin](#erasecacheondetailmixin)
  - [ManageCacheMixin](#managecachemixin)
  - [FullManageCacheMixin](#fullmanagecachemixin)
  - [ByAuthToken Variations](#byauthtoken-variations)
  - [ByUser Variations](#byuser-variations)
- [docs](#docs)
  - [build_list_docs](#build_list_docs)
  - [build_create_docs](#build_create_docs)
  - [build_retrieve_docs](#build_retrieve_docs)
  - [build_update_docs](#build_update_docs)
  - [build_destroy_docs](#build_destroy_docs)
  - [build_list_create_docs](#build_list_create_docs)
  - [build_retrieve_update_destroy_docs](#build_retrieve_update_destroy_docs)
  - [build_docs](#build_docs)
  - [build_docs_by_group](#build_docs_by_group)
  - [build_full_docs](#build_full_docs)
- [action\_patterns](#action_patterns)
- [admin](#admin)
  - [CustomUserAdmin](#customuseradmin)

## Installation

First, run:

```bash
pip install django-awesome-tools
```

That's it!

---

## helpers

This module provides three useful functions. Two of them are a more powerful and versatille version of `get_object_or_404` and `get_list_or_404`, and the other is a handy shortcut.

### get_object_or_error

Almost the same as `django.shortcuts.get_object_or_404`, but can raise any
custom error class you want, allowing you to return more precise error messages.
Another advantage of using this helper function, is that it prevents your application
from crashing. For instance, in case you want to get an object by it's primary key, and
it is of type `uuid`, but another data type is provided in the url, it will not crash,
unlike the standard `get_object_or_404`. It expects the following arguments:

- `klass` -> The model that will be used for the query
- `exception` -> An error class inheriting from `rest_framework.exceptions.APIException`.
If no `exception` is provided, then the standard `django.http.Http404` class is used.
- `**kwargs` -> Keyword arguments representing all fields that should be used for the
search, as many as you please.

For instance, in case you want to get a `Room` of a `Cinema`:

```python
# exceptions.py

from rest_framework.exceptions import APIException, status


class CinemaNotFoundError(APIException):
    status_code = status.HTTP_404_NOT_FOUND
    default_detail = "Cinema not found"


class RoomNotFoundError(APIException):
    status_code = status.HTTP_404_NOT_FOUND
    default_detail = "Room not found in this cinema"
```

```python
# request endpoint

"/cinemas/<cinema_id>/rooms/<room_id>/"
```

```python
# views.py

from awesome_tools.helpers import get_object_or_error


cinema = get_object_or_error(Cinema, CinemaNotFoundError, pk=self.kwargs['cinema_id'])
room = get_object_or_error(Room, RoomNotFoundError, pk=self.kwargs['room_id'], cinema=cinema)
```

Note that in case a room id is valid, but the cinema id is not, an appropriated message will be
returned. In case you would use `get_object_or_404`, you would get just a `"Not found."`. Having
more than one lookup field, `get_object_or_error` makes much clearer what is the problem.

I highly encorage you to have a quick look at the source code, it's quite a simple concept.

##

### get_list_or_error

Almost the same as `django.shortcuts.get_list_or_404`, but can raise any
custom error class you want, allowing you to return more precise error messages.
Another advantage of using this helper function, is that it prevents your application
from crashing. For instance, in case you want to get a list, filtering it by some foreign
key field, which is of type `uuid`, but another data type is provided in the url, it will
not crash, unlike the standard `get_list_or_404`. Also, this function gives you the possiblity
of not raising an exception when no values are found, so you could just return an empty list.
It expects the following arguments:

- `klass` -> The model that will be used for the query
- `exception` -> An error class inheriting from `rest_framework.exceptions.APIException`.
If no `exception` is provided, then the standard `django.http.Http404` class is used.
- `accept_empty` -> A boolean argument, which defaults to `False`. When provided, determines
if an empty result is acceptable or if it should raise `exception`.
- `**kwargs` -> Keyword arguments representing all fields that should be used for the
search, as many as you please.

For instance, in case you want to list all `MovieSession`s of a `Room` in a `Cinema`:

```python
# exceptions.py

from rest_framework.exceptions import APIException, status


class NoMovieSessionsError(APIException):
    status_code = status.HTTP_404_NOT_FOUND
    default_detail = "This room has no scheduled movie sessions"
```

```python
# request endpoint

"/cinemas/<cinema_id>/rooms/<room_id>/movie-sessions/"
```

```python
# views.py

from awesome_tools.helpers import get_object_or_error, get_list_or_error


cinema = get_object_or_error(Cinema, CinemaNotFoundError, pk=self.kwargs['cinema_id'])
room = get_object_or_error(Room, RoomNotFoundError, pk=self.kwargs['room_id'], cinema=cinema)
movie_sessions = get_list_or_error(MovieSession, NoMovieSessionsError, room=room)
```

I highly encorage you to have a quick look at the source code, it's quite a simple concept.

##

### set_and_destroy

This function basically sets a new list of values in a foreign key field and erases any
previous values that were related to `klass`. For it to work, **you must set `null=True`
in your model**, otherwise, the values will not be subsitituted, they will only be added.
It accepts the following parameters:

- `klass` -> The model on the side `1` of a `1:N` relationship, the owner of the relation,
in which the new values will be set
- `attr` -> A string version of the attribute corresponding to the `related_name` value
in the foreign key field
- `value` -> A list (or any other iterable), containing new created instances of `related_klass`
- `related_klass` -> The model on the side `N` of a `1:N` relationship, the one having the foreign
key field
- `**kwargs` -> Keyword arguments used in a filter to determine which objects should be destroyed.
It could be really anything, but usually you will want it to be something like `klass=None`, so
that all objects that are no part of the relationship anymore can be descarded.

For instance, a `Movie` may have many `Video`s related to it, like teasers and trailers. In case
you want to update a `Movie`, reseting its `Video`s:

```python
# models.py

class Movie(models.Model):
    ...


class Video(models.Model):
    id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
    title = models.CharField(max_length=127)
    url = models.URLField()

    movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name="videos", null=True)
```

```python
# serializers.py

from awesome_tools.helpers import set_and_destroy


class MovieSerializer(serializers.ModelSerializer):
    ...

    def update(self, instance: Movie, validated_data: dict):
      ...

      videos_data = validated_data.pop("videos", None)
      if videos_data:
          videos = [
            Video.get_or_create(**video, movie=instance)[0]
            for video in videos_data
          ]
          set_and_destroy(
              klass=instance,
              attr="videos",
              value=videos,
              related_klass=Video,
              movie=None,
          )
```

In the example above, we are first getting or creating video instances, in order to reuse the ones
passed in the body of the request that may already be in our db. Each video can only be related to
one movie, since it doesn't make sense that two movies have the same trailer or teaser. So when
assigning this new list of videos to a movie, the `set_and_destroy` function safely deletes all
videos having their `movie` foreign key equal to `None`.

I highly encorage you to have a quick look at the source code, it's quite a simple concept.

##

### bulk_get_or_create

Despite the name of this function, it does not translate into a single database hit,
unfortunatelly. But it is still better than a loop executing `Model.objects.get_or_create`
in every iteration.

That's because this function **combines filters and the bulk_create method**.
Django querysets are lazy, but in this function they are evaluated on every iteration.
However, in the end **only one** `INSERT` query is performed.

---

#### Important!
Django's `Model.objects.bulk_create` method returns a list of newly created instances **without ids**
when working with _SQLite_. Please, make sure to use _PostgreSQL_ to avoid problems.

---

It expects the following parameters:

- `klass` -> The model whose values will be retrieved or created
- `values` -> A list of dictionaries having key value pairs demanded by `klass`
- `only_create` -> A boolean value. Defaults to `False`. In case you don't care about getting
existing values, and just wants to create them, then you can set this arguments to `True`. It
will result in just one database hit.
- `kwargs` -> Key value pairs with extra fields you want to use for filtering/creating instances
of `klass`. It can be useful for foreign key fields

Usage example:

```python
# serializers.py

from awesome_tools.helpers import bulk_get_or_create, set_and_destroy


class MovieSerializer(serializers.ModelSerializer):
    # ...

    def create(self, validated_data: dict) -> Movie:
        # ...

        videos_data = validated_data.pop("videos")

        # ...

        bulk_get_or_create(Video, videos_data, movie=movie)

        # ...

    def update(self, instance: Movie, validated_data: dict) -> Movie:
        # ...

        videos = validated_data.pop("videos", None)

        # ...

        if videos:
            set_and_destroy(
                klass=instance,
                attr="videos",
                value=bulk_get_or_create(Video, videos, movie=instance),
                related_klass=Video,
                movie=None,
            )

        # ...
```

Note that in the `update` method, we are combining `set_and_destroy` with `bulk_get_or_create`.
That's totally a thing.

I highly encourage you to have a look at the source code, so that you can better understand what's
happening under the hood. It's not complicated.

---

## mixins

This module provides useful mixins to be used in Django Rest Framework **generic views** and **viewsets**.

### SerializerByMethodMixin

This mixin overrides the `get_serializer_class` method of generic views. It's
purpose is to dinamically define which serializer to use, depending on the request
method. For this to be possible, a new class property should be set, it is:

- `method_serializers` -> It should be a dictionary having it's keys with the names
of http methods and values as the serializer classes corresponding to each method.
If the request method does not match any of the dict keys, it will return the value
of `self.serializer_class`.

Below is an example:

```python
# views.py

from awesome_tools.mixins import SerializerByMethodMixin


class MyBeautifulGenericView(SerializerByMethodMixin, ListCreateAPIView):
    queryset = MyWonderfulModel.objects.all()
    serializer_class = MyDefaultSerializer
    method_serializers = {
        "GET": MySerialzerToUseInGetRequests,
    }
```

##

### SerializerByActionMixin

This mixin overrides the `get_serializer_class` method of viewsets. It's
purpose is to dinamically define which serializer to use, depending on the viewset
action. For this to be possible, a new class property should be set, it is:

- `action_serializers` -> It should be a dictionary having it's keys with the names
of viewset actions and values as the serializer classes corresponding to each action.
If the viewset action does not match any of the dict keys, it will return the value
of `self.serializer_class`.

Below is an example:

```python
# views.py

from awesome_tools.mixins import SerializerByActionMixin


class MyBeautifulViewSet(SerializerByActionMixin, ModelViewSet):
    queryset = MyWonderfulModel.objects.all()
    serializer_class = MyDefaultSerializer
    action_serializers = {
        "create": MySerializerToUseInCreateActions,
        "update": MySerialzerToUseInUpdateActions,
        "partial_update": MySerialzerToUseInPartialUpdateActions,
    }
```

##

### SerializerByDetailActionsMixin

This mixin overrides the `get_serializer_class` method of viewsets. It's
purpose is to dinamically define which serializer to use, depending on the viewset
action. If it is a detail action, that is, one of `retrieve`, `update`, `partial_update`
and `destroy`, then `self.detail_serializer_class` will be returned. Else, the default
`self.serializer_class` is used. For this to be possible, a new class property should
be set, it is:

- `detail_serializer_class` -> It's value should be a serializer class. This property defines
which serializer to use in detail actions.

Below is an example:

```python
# views.py

from awesome_tools.mixins import SerializerByDetailActionsMixin


class MyBeautifulViewSet(SerializerByDetailActionsMixin, ModelViewSet):
    queryset = MyWonderfulModel.objects.all()
    serializer_class = MyDefaultSerializer
    detail_serializer_class = MyDetailSerializer
```

##

### SerializerBySafeActionsMixin

This mixin overrides the `get_serializer_class` method of viewsets. It's
purpose is to dinamically define which serializer to use, depending on the viewset
action. If it is a _safe action_, then `self.safe_serializer_class` will be returned.
Else, the default `self.serializer_class` is returned. A safe action is an action
listed in the `safe_actions` class property. For this to be possible, a new class
property should be set, it is:

- `safe_serializer_class` -> Its value should be a serializer class. This property defines
which serializer to use in safe actions.

You can totally customize what is a "safe action". For that, you could change the value
of `self.safe_actions`.

- `safe_actions` -> It should be a `list[str]`, which each item representing a viewset action,
considered safe for that viewset. The default value is `["list", "retrieve"]`

Below is an example:

```python
# views.py

from awesome_tools.mixins import SerializerBySafeActionsMixin


class MyBeautifulViewSet(SerializerBySafeActionsMixin, ModelViewSet):
    queryset = MyWonderfulModel.objects.all()
    serializer_class = MyDefaultSerializer
    safe_serializer_class = MySafeSerializer
```

##

### FilterQuerysetMixin

This mixin overrides the `get_queryset` method of class based views. It's main goal is
to make it easier and simpler to filter and/or narrow down results. You may use it to
attach results to the logged in user, to filter the queryset by route params (or `kwargs`)
and by query params.

These are the class properties that this mixin accepts:

- `filter_user_key` -> A `str` representing which keyword argument should be used for filtering by
user. The default is `None`, meaning that the queryset will not be filtered by the logged in user, that
is, `self.request.user`. If in your queryset there is a `FK` pointing to your project's auth user model, then this property should
have the same name as this `FK` field.
- `filter_kwargs` -> A `dict[str, str]`, where the **key** represents the name of the **field** to be searched,
and the **value** is the **url param**.
- `filter_query_params` -> A `dict[str, str]`, where the **key** is the name of the **field** to be searched,
and the **value** represents the **query param** received in the request.
- `filter_exception_klass` -> Should be an `exception` inheriting from `rest_framework.exceptions.APIException`. The
default value is `django.http.Http404`. In case no value is returned or another kind of error occurs, this
exception will be raised.
- `filter_accept_empty` -> A `bool`, which defaults to `True`. If `False`, then the `exception_klass` will be raised
in case the results are empty. Otherwise, an empty value will be returned normaly.

Below is an example of how this might be useful:

```python
# request endpoint

"/categories/<category_id>/transactions/"

```

```python
# views.py

from awesome_tools.mixins import FilterQuerysetMixin

class TransactionView(FilterQuerysetMixin, ListCreateAPIView):
    serializer_class = TransactionSerializer
    permission_classes = [IsAuthenticated]
    filter_user_key = "user"
    filter_kwargs = {"category": "category_id"}
    filter_query_params = {
        "month_id": "month_id",
        "month__number": "month_number",
        "month__name__icontains": "month_name",
        "month__year": "year",
        "description__icontains": "description",
        "value": "value",
        "value__gte": "value_gte",
        "value__lte": "value_lte",
        "is_income": "is_income",
        "is_recurrent": "is_recurrent",
        "installments": "installments",
    }
    filter_order_by = ["-created_at"]
```

In the example above, we are defining a view for monetary transactions. We don't want
users to see other user's transactions, so we attach all transactions to the logged in
user. By using the `filter_user_key` class property, we tell the mixin that when filtering the
queryset, it should use `user=self.request.user`.

Also, all transactions have categories. And we want them always to be listed by category.
So in the url, we receive the `<category_id>` param. So that's why we declare `filter_kwargs`
in that way.

As for the `filter_query_params` property, please note how interesting it is. In the **keys** of
the dictionary, we pass in the keys that will be used for filtering the queryset, just as if
we were filtering the queryset manually. And the **values** correspond to the query param that we
expect to receive in the request. None of these query params are mandatory.

We are not declaring `filter_accept_empty`, which means that we will not raise `filter_exception_klass`
in any case (because the default value is `True`). So that's why we don't need to define `filter_exception_klass` too.

Furthermore, we are ordering the queryset results by their creation date (in descending order) with the
`filter_order_by` property.

You may have noticed that the `queryset` class property haven't been defined. That's not a
problem, because this mixin guesses what is the apropriated model by accessing `self.serializer_class.Meta.model`.
So as long as you define you model in that way, everything is OK.

##

### AttachUserOnCreateMixin

This mixin overrides the `perform_create` method of generic views, and simply passes to the serializer
`save` method an additional keyword argument. This attaches the current user to the `validated_data`
argument on the serializer's `create` method. You can pass the following class property:

`attach_user_key` -> A `str`, which defaults to `None`. It represents which is the name of the field
that points to the user on your model. If ommited, it will try to get the value of `self.filter_user_key`.

So in case you are already using this module's `FilterQuerysetMixin`, and is using this property, then there
is no need to repeat yourself here. But in case neither `self.attach_user_key` or `self.filter_user_key` are
found, then `"user"` is used by default.
    
Here is a quick example:

```python

from awesome_tools.mixins import AttachUserOnUpdateMixin
from rest_framework import generics
from rest_framework import permissions

from .serializers import CinemaSerializer

class CinemaView(AttachUserOnUpdateMixin, generics.ListCreateAPIView):
    serializer_class = CinemaSerializer
    permission_classes = [permissions.IsAuthenticated]
    attach_user_key = "owner"

```

This simple trick makes it possible to attach an user to a `Cinema` instance very easily. In this case, we are
defining `attach_user_key` as `"owner"`, because on the `Cinema` model, the foreign key field that relates to
the user model, has this name.

##

### AttachUserOnUpdateMixin

Exactly the same as [AttachUserOnCreateMixin](#attachuseroncreatemixin), but overrides the `perform_update`
method.

##

### AttachUserToReqDataMixin

A combination of [AttachUserOnCreateMixin](#attachuseroncreatemixin) and [AttachUserOnUpdateMixin](#attachuseronupdatemixin), 
overriding both `perform_create` and `perform_update` methods of generic views.


---

## managers

This module provides a custom user manager as a shortcut for whoever wants to customize
django's authentication system to use a different field instead of username for login.
It can be really anything, like email, phone, cpf, etc.

### CustomUserManager

A custom user manager that inherits from `django.contrib.auth.models.BaseUserManager`.
Its purpouse in life is mainly to provide an easy and simple way to implement a login
and register system that expects another field instead of `username`.

But what if you desired to customize your users in a way that other info is also required
for user creation? No problem, this class is highly customizable.

Instead of having to override the `create` and `create_superuser` methods of `BaseUserManager`,
you can inherit from `CustomUserManager` and then simply set some class properties at your will.
They work as follows:

- `auth_field_name` -> Defaults to `"email"`. Defines what is the name of the field that
should be used for login (besides password, of course). Note that this field **must**
exist in your user model, **having a unique constraint**.
- `user_is_staff` -> Defaults to `False`. Defines the starting staff status of newly
created users
- `user_start_active` -> Defaults to `True`. Defines if a user account should start in
active state. In cases where users have to confirm their account in some way before getting
access, you may wish to set this property to `False`
- `super_is_staff` -> Defaults to `True`. Defines the starting staff status of newly
created superusers
- `super_start_active` -> Defaults to `True`. Defines if a superuser account should start in
active state. Usually you'll want this value to be `True`, but you're totally free to change
it, depending on your needs.
- `required_fields` -> Defaults to `[]`. It should be a `list[str]`. This property defines
which fields are required to be provided upon user creation, besides `self.auth_field_name` and
`password`. The fields `is_staff`, `is_superuser` and `is_active` should also not be present in
this list. It is worth noting that **all fields defined here, must also be defined in your user model**.
Otherwise, a `ValidationError` is raised.

Below is an example of how you may customize the behaviour of this class:

```python
# managers.py

from awesome_tools.managers import CustomUserManager


class MyOwnUserManager(CustomUserManager):
    user_start_active = False
    required_fields = ["first_name", "last_name"]
```

In order to implement a login with email feature, for instance, you have to make some minor
changes to your user model. Below are some settings that may come in handy for you to define
in your model:

```python
# models.py

from .managers import MyOwnUserManager
from django.db import models
from django.contrib.auth.models import AbstractUser


class MyUser(AbstractUser):
    email = models.EmailField(unique=True)

    username = None

    objects = MyOwnUserManager()

    USERNAME_FIELD = objects.auth_field_name
    REQUIRED_FIELDS = objects.required_fields
```

The `email` property is defined as unique, since it's gonna be used for login (as per the `USERNAME_FIELD`
property). The `objects` property may be either the standard `awesome_tools.managers.CustomUserManager`
or your own manager that inherits from it. In the example above, we are using our own user manager,
with some minor customizations. `REQUIRED_FIELDS` refer to the fields you are prompted when creating a
superuser (it must not include the value defined for `USERNAME_FIELD` or `"password"`). Defining it to
`objects.required_fields` prevents you from making mistakes and being redundant. Note that in the example
above we are droping the `username` column, but that's not necessary if you still want to have a username
in your user model.

---

## cache

This subpackage provides a set of useful mixins that may be used for cache management. It also provides a function
for building your own custom mixins.

### build_cache_mixins

This function returns a tuple of mixins used for cache management. It receives the following arguments:
    
- `cache_ttl` -> The ttl(time to live) for the cache is by default whatever you
set in the `CACHE_TTL` variable at your project's `settings.py`, but you can totally override this here.
The value of `cache_ttl` must be an `int`. It represents the time that the cache will persist, **in seconds**.
In case `CACHE_TTL` is not present in `settings.py`, then it defaults to 10 minutes.
- `vary_on_headers` -> This argument is a `tuple` that refers to which headers should be used when generating
the cache key and cache group.
- `vary_on_user` -> A boolean value that determines if the cache key and cache group should be isolated for each user.

It is important to note that, besides the value of `vary_on_headers` and `vary_on_user`, cache keys are generated
based on the request path and query params, and cache groups are generated based on the request path.

Here is a simple example of how you could use it:

```python

from awesome_tools.cache import build_cache_mixins

(
    SetCacheOnListByMyCoolHeaderMixin,
    EraseCacheOnCreateByMyCoolHeaderMixin,
    EraseCacheOnUpdateByMyCoolHeaderMixin,
    EraseCacheOnDestroyByMyCoolHeaderMixin,
    EraseCacheOnDetailByMyCoolHeaderMixin,
    ManageCacheByMyCoolHeaderMixin,
    FullManageCacheByMyCoolHeaderMixin,
) = build_cache_mixins(vary_on_headers=("my-cool-header",))

```

---

In the example above, if the same request is made again on a cached view, but with a different value on `"my-cool-header"`
header, then the view will not use the cached value, rather, it will cache the results also based on this header.

Actually, this illustrates exactly how the cache management mixins on this package are generated.

##

### SetCacheOnListMixin

Caches the results of the `list` method of generic views and viewsets. After setting the cache,
if the same request is fired again, then it will return the cached value, instead of doing the
whole thing again.

Here is a simple example of how you could use this mixin.

```python

from awesome_tools.cache import SetCacheOnListMixin
from rest_framework.generics import ListAPIView


class MyAwesomeListView(SetCacheOnListMixin, ListAPIView):
    # my awesome view stuff
    ...

```

##

### EraseCacheOnCreateMixin

Upon calling the `create` method of generic views and viewsets, erase the cache. But what cache?
The cache related to the group this mixin belongs to. It is by default determined by the url path,
but may vary based on user or any headers on the request, if these arguments are passed to the
`build_cache_mixins` function.

Here is an example:

```python

from awesome_tools.cache import SetCacheOnListMixin, EraseCacheOnCreateMixin
from rest_framework.generics import ListAPIView, CreateAPIView


class MyAwesomeListView(SetCacheOnListMixin, ListAPIView):
    # my awesome view stuff
    ...

class MyAwesomeCreateView(EraseCacheOnCreateMixin, CreateAPIView):
    # my awesome view stuff
    ...

```

In the example above, when the create view is called, then it will erase any cache keys that were set
by the list view, if there are any, but **only within the scope of the cache group**.

##

### EraseCacheOnUpdateMixin

Exactly the same as [EraseCacheOnCreateMixin](#erasecacheoncreatemixin), but with `update` and `partial_update`
methods of generic views and viewsets.

##

### EraseCacheOnDestroyMixin

Exactly the same as [EraseCacheOnCreateMixin](#erasecacheoncreatemixin) and [EraseCacheOnUpdateMixin](#erasecacheonupdatemixin),
but with the `destroy` method of generic views and viewsets.

##

### EraseCacheOnDetailMixin

This is just a combination of both [EraseCacheOnUpdateMixin](#erasecacheonupdatemixin) and [EraseCacheOnDestroyMixin](#erasecacheondestroymixin).

Here is an example:
        
```python

from awesome_tools.cache import SetCacheOnListMixin, EraseCacheOnDetailMixin
from rest_framework.generics import ListAPIView, RetrieveUpdateDestroyAPIView


class MyAwesomeListView(SetCacheOnListMixin, ListAPIView):
    # my awesome view stuff
    ...

class MyAwesomeDetailView(EraseCacheOnDetailMixin, RetrieveUpdateDestroyAPIView):
    # my awesome view stuff
    ...

```

In the example above, when the detail view is called, then it will erase any cache keys that were set
by the list view, if there are any, but **only within the scope of the cache group**.

##

### ManageCacheMixin

Upon calling the `list` method of the view, set the cache, but when calling the `create` method, then erase the cache.
This mixin is essentially just a combination of both [SetCacheOnListMixin](#setcacheonlistmixin) and [EraseCacheOnCreateMixin](#erasecacheoncreatemixin).

Here is an example:

```python

from awesome_tools.cache import ManageCacheMixin
from rest_framework.generics import ListCreateAPIView


class MyAwesomeView(ManageCacheMixin, ListCreateAPIView):
    # my awesome view stuff
    ...

```

In the example above, when the `list` method of the view is called, then it will set the cache, but when the `create`
method is called, then it will erase any cache keys that were set on the `list` method, if there are any, but
**only within the scope of the cache group**. The cache group is by default determined by the url path, but may vary
based on user or any headers on the request, if these arguments are passed to the `build_cache_mixins` function.

##

### FullManageCacheMixin

Upon calling the `list` method of the view, set the cache, but when calling `create`, `update`, `partial_update` and
`destroy` methods, then erase the cache. This mixin is essentially just a combination of both [ManageCacheMixin](#managecachemixin)
and [EraseCacheOnDetailMixin](#erasecacheondetailmixin).

Here is an example:

```python

from awesome_tools.cache import FullManageCacheMixin
from rest_framework.viewsets import ModelViewSet


class MyAwesomeViewSet(FullManageCacheMixin, ModelViewSet):
    # my awesome viewset stuff
    ...

```

In the example above, when the `list` method of the viewset is called, then it will set the cache, but when any of
`create`, `update`, `partial_update` and `destroy` methods is called, then it will erase any cache keys that were
set on the `list` method, if there are any, but **only within the scope of the cache group**. The cache group is
by default determined by the url path, but may vary based on user or any headers on the request, if these arguments
are passed to the `build_cache_mixins` function.

##

### ByAuthToken Variations

### SetCacheOnListByAuthTokenMixin

Exactly the same as [SetCacheOnListMixin](#setcacheonlistmixin), but grouping the cache by the `"Authorization"` header.
This means that the cache will be isolated by the value of the auth token, even within the scope of the same user.

The following mixins are all variations of the mixins described until now, just like the one above. They all group the
cache by `"Authorization"` header. They are as follows:

- ### EraseCacheOnCreateByAuthTokenMixin
- ### EraseCacheOnUpdateByAuthTokenMixin
- ### EraseCacheOnDestroyByAuthTokenMixin
- ### EraseCacheOnDetailByAuthTokenMixin
- ### ManageCacheByAuthTokenMixin
- ### FullManageCacheByAuthTokenMixin

##

### ByUser Variations

### SetCacheOnListByUserMixin

Exactly the same as [SetCacheOnListMixin](#setcacheonlistmixin), but grouping the cache by `request.user`. This means that
the cache will be isolated by user, even if the authorization token may expire.

The following mixins are all variations of the mixins described until now, just like the one above. They all group the
cache by `request.user`. They are as follows:

- ### EraseCacheOnCreateByUserMixin
- ### EraseCacheOnUpdateByUserMixin
- ### EraseCacheOnDestroyByUserMixin
- ### EraseCacheOnDetailByUserMixin
- ### ManageCacheByUserMixin
- ### FullManageCacheByUserMixin

---

## docs

This module provides a set of useful and simple to use functions for building documentation mixins.
But wait, what is a "documentation mixin"? The `drf-spectacular` package has a wonderful feature of
automatically generating a swagger documentation, based on your project views, with minimum configurations
on `settings.py` and `urls.py`. But maybe you want to give more details on how each view and endpoint works,
such as a brief summary, a detailed description, or maybe specify which query parameters it accepts. For that,
`drf-spectacular` library provides some ways for us to apply such customizations.

Among them, is the `drf_spectacular.utils` module. It has a decorator called `extend_schema`, which can be used
to decorate view methods that correspond to http methods, like `get`, `post` and so forth. It accepts many arguments.

The functions defined in this module use this decorator to build mixins that can be easily aplied to any class based view.
It is recommended to put these mixins on the far left of your views inheritance list, as show in the examples below.

### build_list_docs

Create a mixin class that applies the `drf_spectacular.utils.extend_schema` decorator to the
`get` method of views. It receives the following arguments:

`summary` -> A `str`, which is a brief description of what that endpoint does.
`description` -> Also a `str`, which can be used to provide further details on the behavior of the
endpoint.
`parameters` -> A `list` filled with values of type `OpenApiParameter`.
`*args, **kwargs` -> Any other parameters that `@extend_schema` expects.

Here's an example:

```python
from awesome_tools.docs import build_list_docs
from drf_spectacular.utils import OpenApiParameter

summary = "A wonderful and brief description of the list action on an endpoint"

description = (
    "A more detailed description of the list action and some tips on how to use it. "
    "Here I could add anything more that I may wish to appear on swagger."
)

parameters = [
    OpenApiParameter("param1", int, description="A brief description of my int query param"),
    OpenApiParameter("param2", bool, description="A brief description of my bool query param"),
]

MyListViewDocsMixin = build_list_docs(summary, description, parameters)
```

```python
# views.py

class MyListView(MyListViewDocsMixin, ListAPIView):
    # view stuff
    ...
```

##

### build_create_docs

Same as above, but with the `post` http method, for create views

##

### build_retrieve_docs

Same as above, but with the `get` http method, for retrieve views

##

### build_update_docs

Same as above, but with the `put` and `patch` http methods, for update views

##

### build_destroy_docs

Same as above, but with the `destroy` http method, for destroy views

##

### build_list_create_docs

Just a combination of `build_list_docs` and `build_create_docs`. Returns a single mixin, that combine
both `ListDocsMixin` and `CreateDocsMixin`, which are the return values of these two functions, respectively.
It may come in handy for `ListCreate` views. It receives the following arguments:

`summaries` -> A `dict`, containing the summaries of each endpoint, where the keys are any of `["list", "create"]`, 
and the values are the summaries.
`descriptions` -> A `dict`, containing the desciptions of each endpoint, where the keys are any of `["list", "create"]`, 
and the values are the desciptions.
`parameters` -> A `list` filled with values of type `OpenApiParameter`, to be used on the `list` endpoint.
`*args, **kwargs` -> Any other parameters that `@extend_schema` expects (note that this will be applied to both methods).

Here's an example:

```python
from awesome_tools.docs import build_list_create_docs
from drf_spectacular.utils import OpenApiParameter

summaries = {
    "list": "A wonderful and brief description of the list action on an endpoint",
    "create": "A wonderful and brief description of the create action on an endpoint",
}

descriptions = {
    "create": (
        "A more detailed description of the create action and some tips on how to use it. "
        "Here I could add anything more that I may wish to appear on swagger."
    )
}

parameters = [
    OpenApiParameter("param1", int, description="A brief description of my int query param"),
    OpenApiParameter("param2", bool, description="A brief description of my bool query param"),
]

MyViewDocsMixin = build_list_create_docs(summaries, descriptions, parameters)
```

```python
# views.py

class MyView(MyViewDocsMixin, ListCreateAPIView):
    # view stuff
    ...
```

##

### build_retrieve_update_destroy_docs

Just a combination of `build_retrieve_docs` and `build_update_docs` and `build_destroy_docs`. Returns a single
mixin, that combine `RetrieveDocsMixin`, `UpdateDocsMixin` and `DestroyDocsMixin`, which are the return values
of these three functions, respectively. It may come in handy for `RetrieveUpdateDestroy` views. It receives the
following arguments:

`summaries` -> A `dict`, containing the summaries of each endpoint, where the keys are any of `["retrieve", "update", "destroy"]`, 
and the values are the summaries.
`descriptions` -> A `dict`, containing the desciptions of each endpoint, where the keys are any of `["retrieve", "update", "destroy"]`, 
and the values are the desciptions.
`*args, **kwargs` -> Any other parameters that `@extend_schema` expects (note that this will be applied to all methods).

Here's an example:

```python
from awesome_tools.docs import build_retrieve_update_destroy_docs

summaries = {
    "retrieve": "A wonderful and brief description of the retrieve action on an endpoint",
    "update": "A wonderful and brief description of the update action on an endpoint",
    "destroy": "A wonderful and brief description of the delete action on an endpoint",
}

descriptions = {
    "destroy": (
        "A long description of the destroy action and a warning about its consequences"
        "Here I could add anything more that I may wish to appear on swagger."
    )
}

MyDetailViewDocsMixin = build_retrieve_update_destroy_docs(summaries, descriptions)
```

```python
# views.py

class MyDetailView(MyDetailViewDocsMixin, RetrieveUpdateDestroyAPIView):
    # view stuff
    ...
```

##

### build_docs

Return a tuple of five mixins, each of which are built using `build_list_docs`, `build_create_docs`
`build_retrieve_docs`, `build_update_docs` and `build_destroy_docs`, respectively. It receives the following arguments:

`summaries` -> A `dict`, containing the summaries of each endpoint, where the keys are any of
`["list", "create", "retrieve", "update", "destroy"]`, and the values are the summaries.
`descriptions` -> A `dict`, containing the desciptions of each endpoint, where the keys are any of
`["list", "create", "retrieve", "update", "destroy"]`, and the values are the desciptions.
`parameters` -> A `list` filled with values of type `OpenApiParameter`, to be used on the `list` endpoint.
`*args, **kwargs` -> Any other parameters that `@extend_schema` expects (note that this will be applied to all methods).

Here's an example:

```python
from awesome_tools.docs import build_docs
from drf_spectacular.utils import OpenApiParameter

summaries = {
    "list": "A wonderful and brief description of the list action on an endpoint",
    "create": "A wonderful and brief description of the create action on an endpoint",
    "retrieve": "A wonderful and brief description of the retrieve action on an endpoint",
    "update": "A wonderful and brief description of the update action on an endpoint",
    "destroy": "A wonderful and brief description of the delete action on an endpoint",
}

descriptions = {
    "destroy": (
        "A long description of the destroy action and a warning about its consequences"
        "Here I could add anything more that I may wish to appear on swagger."
    )
}

parameters = [
    OpenApiParameter("param1", int, description="A brief description of my int query param"),
    OpenApiParameter("param2", bool, description="A brief description of my bool query param"),
]

(
    MyListViewDocsMixin,
    MyCreateViewDocsMixin,
    MyRetrieveViewDocsMixin
    MyUpdateViewDocsMixin,
    MyDestroyViewDocsMixin,
) = build_docs(summaries, descriptions, parameters)
```

```python
# views.py

class MyListView(MyListViewDocsMixin, ListAPIView):
    # view stuff
    ...

class MyCreateView(MyCreateViewDocsMixin, CreateAPIView):
    # view stuff
    ...

# and so forth...
```

##

### build_docs_by_group

Return a tuple with two mixins, which of which are built using `build_list_create_docs` and
`build_retrieve_update_destroy_docs`, respectively. This may come in handy in those cases where
you have two classes, one is a `ListCreateView` and the other is a `RetrieveUpdateDestroyView`.
It receives the following arguments:

`summaries` -> A `dict`, containing the summaries of each endpoint, where the keys are any of
`["list", "create", "retrieve", "update", "destroy"]`, and the values are the summaries.
`descriptions` -> A `dict`, containing the desciptions of each endpoint, where the keys are any of
`["list", "create", "retrieve", "update", "destroy"]`, and the values are the desciptions.
`parameters` -> A `list` filled with values of type `OpenApiParameter`, to be used on the `list` endpoint.
`*args, **kwargs` -> Any other parameters that `@extend_schema` expects (note that this will be applied to all methods).

Here's an example:

```python
from awesome_tools.docs import build_docs_by_group
from drf_spectacular.utils import OpenApiParameter

summaries = {
    "list": "A wonderful and brief description of the list action on an endpoint",
    "create": "A wonderful and brief description of the create action on an endpoint",
    "retrieve": "A wonderful and brief description of the retrieve action on an endpoint",
    "update": "A wonderful and brief description of the update action on an endpoint",
    "destroy": "A wonderful and brief description of the delete action on an endpoint",
}

descriptions = {
    "destroy": (
        "A long description of the destroy action and a warning about its consequences"
        "Here I could add anything more that I may wish to appear on swagger."
    )
}

parameters = [
    OpenApiParameter("param1", int, description="A brief description of my int query param"),
    OpenApiParameter("param2", bool, description="A brief description of my bool query param"),
]

(
    MyViewDocsMixin,
    MyDetailViewDocsMixin,
) = build_docs_by_group(summaries, descriptions, parameters)
```

```python
# views.py

class MyView(MyViewDocsMixin, ListCreateAPIView):
    # view stuff
    ...
```

##

### build_full_docs

Return a single mixin, that is a combination of both `ListCreateDocs` and `RetrieveUpdateDestroyDocs`,
which `build_docs_by_group` returns. It receives the following arguments:

`summaries` -> A `dict`, containing the summaries of each endpoint, where the keys are any of
`["list", "create", "retrieve", "update", "destroy"]`, and the values are the summaries.
`descriptions` -> A `dict`, containing the desciptions of each endpoint, where the keys are any of
`["list", "create", "retrieve", "update", "destroy"]`, and the values are the desciptions.
`parameters` -> A `list` filled with values of type `OpenApiParameter`, to be used on the `list` endpoint.
`*args, **kwargs` -> Any other parameters that `@extend_schema` expects (note that this will be applied to all methods).

    
Here's an example:

```python
from awesome_tools.docs import build_full_docs
from drf_spectacular.utils import OpenApiParameter

summaries = {
    "list": "A wonderful and brief description of the list action on an endpoint",
    "create": "A wonderful and brief description of the create action on an endpoint",
    "retrieve": "A wonderful and brief description of the retrieve action on an endpoint",
    "update": "A wonderful and brief description of the update action on an endpoint",
    "destroy": "A wonderful and brief description of the delete action on an endpoint",
}

descriptions = {
    "destroy": (
        "A long description of the destroy action and a warning about its consequences"
        "Here I could add anything more that I may wish to appear on swagger."
    )
}

parameters = [
    OpenApiParameter("param1", int, description="A brief description of my int query param"),
    OpenApiParameter("param2", bool, description="A brief description of my bool query param"),
]

MyViewFullDocsMixin = build_full_docs(summaries, descriptions, parameters)

```

---

## action_patterns

Viewsets have the advantage of abstracting away the work of defining routes explicitly,
but routers have some limits. They can only go to a certain depth in producing urls.

For instance, let's imagine a simple application, where you have Bands and Albums.
In case you wish to list all Albums of a Band, you could make a request to an enpoint
like `/bands/<band_id>/albums/`. That's totally possible with routers. But what if you
want a detail route for an Album of a Band? A route like `/bands/<band_id>/albums/<album_id>/`
would make sense, right? But routers aren't able to go to such an extent. And you could
totally imagine bigger urls in real, bigger applications.

So defining our routes manually gives us a lot more control. Everything comes with a tradeoff
though. When manually defining routes for generic views, you can easily assign each view class
to their routes, using the `as_view` method. But viewsets are different. One viewset class can
be assigned to more than one route. So for that to work, you've gotta do something like [this](https://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/#binding-viewsets-to-urls-explicitly).

In order to simplify things, and abstract away some boiler plate code, this module provides the
standard viewset actions mapped to their corresponding http method. Of course, you may have additional
actions, customized according to your own needs. In this case, you can config them on your own. But
the standard ones are all set here.

Usage example:

```python
# urls.py

from django.urls import path
from awesome_tools.action_patterns import STANDARD_DETAIL_PATTERN, STANDARD_PATTERN

from . import views


cinema_view = views.CinemaViewSet.as_view(STANDARD_PATTERN)
cinema_detail_view = views.CinemaViewSet.as_view(STANDARD_DETAIL_PATTERN)

urlpatterns = [
    path("", cinema_view),
    path("<cinema_id>/", cinema_detail_view),
]
```

But routers are still so cool and so simple to use. So a very good alternative is [drf-nested-routers](https://github.com/alanjds/drf-nested-routers).
It really makes it easier to deal with all of this. The `drf-nested-routers` library is designed to
solve exactly this problem, and even more.

---

## admin

This module provides a `CustomUserAdmin` class. It inherits from `django.contrib.auth.admin.UserAdmin`.
Have you ever created a custom user model, added it to admin and then realized that your users passwords
were being created unhashed? Then you searched the internet and found out that django provides a `UserAdmin`
class that does the job. But what if you customized your authentication system, and you're using another
field instead of `username`? In this case, it throws an error, saying that there is no `username` field.

In order to make things easier, this module provides a class that abstracts away all the boring 
configurations you would need to do.

### CustomUserAdmin

This class inherits from `django.contrib.auth.admin.UserAdmin`. It's purpose in life is to abstract
away some boring configurations you may need, when you're using a custom user model. The advantage is
to have the same features that Django standard `UserAdmin` class provides, but in a custom user model,
having a field other than `username` used for authentication.

This class automaticaly figures out what is your user model, as long as it is pointed to by `AUTH_USER_MODEL`
setting in `settings.py`. Also, it takes the care of first checking for the fields you set in your user
model before referencing them. But the **password field is mandatory**.

Below is an usage example:

```python
# admin.py

from awesome_tools.admin import CustomUserAdmin
from .models import User

admin.site.register(User, CustomUserAdmin)
```

In case you want to customize some kind of behaviour, you totally can, either by overwriting the properties
entirely (by inheriting this class), or by using one of the class methods defined in this class. For instance,
if you added some columns that are not default of auth user model, but still want them to appear in the admin,
you could do something like this:

```python

# admin.py

from awesome_tools.admin import CustomUserAdmin
from .models import User

fields = ("cpf", "phone")

# add fields to the user creation form
CustomUserAdmin.add_creation_fields(fields)
# append fields to list_display
CustomUserAdmin.add_list_display(fields)
# add fields to personal info screen
CustomUserAdmin.add_personal_info(fields)

admin.site.register(User, CustomUserAdmin)
```

Not so bad.

            

Raw data

            {
    "_id": null,
    "home_page": "",
    "name": "django-awesome-tools",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": "Cayo Rodrigues <cayo.rodrigues1914@gmail.com>",
    "keywords": "django,utils,serializers,generic,views,viewsets,mixins,email,login,model,manager,custom,action,shortcut,error,simple,rest,framework,dj,drf,admin,hash,password,user,filter,queryset,cache,management,by auth token,by user,group,key,docs,documentation,spectacular,customize,swagger,openapi",
    "author": "",
    "author_email": "Cayo Rodrigues <cayo.rodrigues1914@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/88/67/7a59267fbd574fece672990bcbda99ab9b111299ca6338d44c9991a8ea68/django_awesome_tools-1.5.7.tar.gz",
    "platform": null,
    "description": "<div align=\"center\">\n    <img src=\"https://user-images.githubusercontent.com/87717182/222975347-32f01617-71a4-42f3-9db2-248a52ffe819.png\">\n</div>\n<h1 align=\"center\">Django Awesome Tools</h1>\n\nThis package provides useful and powerful functions and classes to be used in [Django](https://www.djangoproject.com/) projects, specially when working with [Django Rest Framework](https://www.django-rest-framework.org/). Below are some further explation about how to use this package and what each module inside it does.\n\n## Table of Contents\n\n- [Installation](#installation)\n- [helpers](#helpers)\n  - [get\\_object\\_or\\_error](#get_object_or_error)\n  - [get\\_list\\_or\\_error](#get_list_or_error)\n  - [set\\_and\\_destroy](#set_and_destroy)\n  - [bulk\\_get\\_or\\_create](#bulk_get_or_create)\n- [mixins](#mixins)\n  - [SerializerByMethodMixin](#serializerbymethodmixin)\n  - [SerializerByActionMixin](#serializerbyactionmixin)\n  - [SerializerByDetailActionsMixin](#serializerbydetailactionsmixin)\n  - [SerializerBySafeActionsMixin](#serializerbysafeactionsmixin)\n  - [FilterQuerysetMixin](#filterquerysetmixin)\n  - [AttachUserOnCreateMixin](#attachuseroncreatemixin)\n  - [AttachUserOnUpdateMixin](#attachuseronupdatemixin)\n  - [AttachUserToReqDataMixin](#attachusertoreqdatamixin)\n- [managers](#managers)\n  - [CustomUserManager](#customusermanager)\n- [cache](#cache)\n  - [build\\_cache\\_mixins](#build_cache_mixins)\n  - [SetCacheOnListMixin](#setcacheonlistmixin)\n  - [EraseCacheOnCreateMixin](#erasecacheoncreatemixin)\n  - [EraseCacheOnUpdateMixin](#erasecacheonupdatemixin)\n  - [EraseCacheOnDestroyMixin](#erasecacheondestroymixin)\n  - [EraseCacheOnDetailMixin](#erasecacheondetailmixin)\n  - [EraseCacheOnDetailMixin](#erasecacheondetailmixin)\n  - [ManageCacheMixin](#managecachemixin)\n  - [FullManageCacheMixin](#fullmanagecachemixin)\n  - [ByAuthToken Variations](#byauthtoken-variations)\n  - [ByUser Variations](#byuser-variations)\n- [docs](#docs)\n  - [build_list_docs](#build_list_docs)\n  - [build_create_docs](#build_create_docs)\n  - [build_retrieve_docs](#build_retrieve_docs)\n  - [build_update_docs](#build_update_docs)\n  - [build_destroy_docs](#build_destroy_docs)\n  - [build_list_create_docs](#build_list_create_docs)\n  - [build_retrieve_update_destroy_docs](#build_retrieve_update_destroy_docs)\n  - [build_docs](#build_docs)\n  - [build_docs_by_group](#build_docs_by_group)\n  - [build_full_docs](#build_full_docs)\n- [action\\_patterns](#action_patterns)\n- [admin](#admin)\n  - [CustomUserAdmin](#customuseradmin)\n\n## Installation\n\nFirst, run:\n\n```bash\npip install django-awesome-tools\n```\n\nThat's it!\n\n---\n\n## helpers\n\nThis module provides three useful functions. Two of them are a more powerful and versatille version of `get_object_or_404` and `get_list_or_404`, and the other is a handy shortcut.\n\n### get_object_or_error\n\nAlmost the same as `django.shortcuts.get_object_or_404`, but can raise any\ncustom error class you want, allowing you to return more precise error messages.\nAnother advantage of using this helper function, is that it prevents your application\nfrom crashing. For instance, in case you want to get an object by it's primary key, and\nit is of type `uuid`, but another data type is provided in the url, it will not crash,\nunlike the standard `get_object_or_404`. It expects the following arguments:\n\n- `klass` -> The model that will be used for the query\n- `exception` -> An error class inheriting from `rest_framework.exceptions.APIException`.\nIf no `exception` is provided, then the standard `django.http.Http404` class is used.\n- `**kwargs` -> Keyword arguments representing all fields that should be used for the\nsearch, as many as you please.\n\nFor instance, in case you want to get a `Room` of a `Cinema`:\n\n```python\n# exceptions.py\n\nfrom rest_framework.exceptions import APIException, status\n\n\nclass CinemaNotFoundError(APIException):\n    status_code = status.HTTP_404_NOT_FOUND\n    default_detail = \"Cinema not found\"\n\n\nclass RoomNotFoundError(APIException):\n    status_code = status.HTTP_404_NOT_FOUND\n    default_detail = \"Room not found in this cinema\"\n```\n\n```python\n# request endpoint\n\n\"/cinemas/<cinema_id>/rooms/<room_id>/\"\n```\n\n```python\n# views.py\n\nfrom awesome_tools.helpers import get_object_or_error\n\n\ncinema = get_object_or_error(Cinema, CinemaNotFoundError, pk=self.kwargs['cinema_id'])\nroom = get_object_or_error(Room, RoomNotFoundError, pk=self.kwargs['room_id'], cinema=cinema)\n```\n\nNote that in case a room id is valid, but the cinema id is not, an appropriated message will be\nreturned. In case you would use `get_object_or_404`, you would get just a `\"Not found.\"`. Having\nmore than one lookup field, `get_object_or_error` makes much clearer what is the problem.\n\nI highly encorage you to have a quick look at the source code, it's quite a simple concept.\n\n##\n\n### get_list_or_error\n\nAlmost the same as `django.shortcuts.get_list_or_404`, but can raise any\ncustom error class you want, allowing you to return more precise error messages.\nAnother advantage of using this helper function, is that it prevents your application\nfrom crashing. For instance, in case you want to get a list, filtering it by some foreign\nkey field, which is of type `uuid`, but another data type is provided in the url, it will\nnot crash, unlike the standard `get_list_or_404`. Also, this function gives you the possiblity\nof not raising an exception when no values are found, so you could just return an empty list.\nIt expects the following arguments:\n\n- `klass` -> The model that will be used for the query\n- `exception` -> An error class inheriting from `rest_framework.exceptions.APIException`.\nIf no `exception` is provided, then the standard `django.http.Http404` class is used.\n- `accept_empty` -> A boolean argument, which defaults to `False`. When provided, determines\nif an empty result is acceptable or if it should raise `exception`.\n- `**kwargs` -> Keyword arguments representing all fields that should be used for the\nsearch, as many as you please.\n\nFor instance, in case you want to list all `MovieSession`s of a `Room` in a `Cinema`:\n\n```python\n# exceptions.py\n\nfrom rest_framework.exceptions import APIException, status\n\n\nclass NoMovieSessionsError(APIException):\n    status_code = status.HTTP_404_NOT_FOUND\n    default_detail = \"This room has no scheduled movie sessions\"\n```\n\n```python\n# request endpoint\n\n\"/cinemas/<cinema_id>/rooms/<room_id>/movie-sessions/\"\n```\n\n```python\n# views.py\n\nfrom awesome_tools.helpers import get_object_or_error, get_list_or_error\n\n\ncinema = get_object_or_error(Cinema, CinemaNotFoundError, pk=self.kwargs['cinema_id'])\nroom = get_object_or_error(Room, RoomNotFoundError, pk=self.kwargs['room_id'], cinema=cinema)\nmovie_sessions = get_list_or_error(MovieSession, NoMovieSessionsError, room=room)\n```\n\nI highly encorage you to have a quick look at the source code, it's quite a simple concept.\n\n##\n\n### set_and_destroy\n\nThis function basically sets a new list of values in a foreign key field and erases any\nprevious values that were related to `klass`. For it to work, **you must set `null=True`\nin your model**, otherwise, the values will not be subsitituted, they will only be added.\nIt accepts the following parameters:\n\n- `klass` -> The model on the side `1` of a `1:N` relationship, the owner of the relation,\nin which the new values will be set\n- `attr` -> A string version of the attribute corresponding to the `related_name` value\nin the foreign key field\n- `value` -> A list (or any other iterable), containing new created instances of `related_klass`\n- `related_klass` -> The model on the side `N` of a `1:N` relationship, the one having the foreign\nkey field\n- `**kwargs` -> Keyword arguments used in a filter to determine which objects should be destroyed.\nIt could be really anything, but usually you will want it to be something like `klass=None`, so\nthat all objects that are no part of the relationship anymore can be descarded.\n\nFor instance, a `Movie` may have many `Video`s related to it, like teasers and trailers. In case\nyou want to update a `Movie`, reseting its `Video`s:\n\n```python\n# models.py\n\nclass Movie(models.Model):\n    ...\n\n\nclass Video(models.Model):\n    id = models.UUIDField(primary_key=True, editable=False, default=uuid4)\n    title = models.CharField(max_length=127)\n    url = models.URLField()\n\n    movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name=\"videos\", null=True)\n```\n\n```python\n# serializers.py\n\nfrom awesome_tools.helpers import set_and_destroy\n\n\nclass MovieSerializer(serializers.ModelSerializer):\n    ...\n\n    def update(self, instance: Movie, validated_data: dict):\n      ...\n\n      videos_data = validated_data.pop(\"videos\", None)\n      if videos_data:\n          videos = [\n            Video.get_or_create(**video, movie=instance)[0]\n            for video in videos_data\n          ]\n          set_and_destroy(\n              klass=instance,\n              attr=\"videos\",\n              value=videos,\n              related_klass=Video,\n              movie=None,\n          )\n```\n\nIn the example above, we are first getting or creating video instances, in order to reuse the ones\npassed in the body of the request that may already be in our db. Each video can only be related to\none movie, since it doesn't make sense that two movies have the same trailer or teaser. So when\nassigning this new list of videos to a movie, the `set_and_destroy` function safely deletes all\nvideos having their `movie` foreign key equal to `None`.\n\nI highly encorage you to have a quick look at the source code, it's quite a simple concept.\n\n##\n\n### bulk_get_or_create\n\nDespite the name of this function, it does not translate into a single database hit,\nunfortunatelly. But it is still better than a loop executing `Model.objects.get_or_create`\nin every iteration.\n\nThat's because this function **combines filters and the bulk_create method**.\nDjango querysets are lazy, but in this function they are evaluated on every iteration.\nHowever, in the end **only one** `INSERT` query is performed.\n\n---\n\n#### Important!\nDjango's `Model.objects.bulk_create` method returns a list of newly created instances **without ids**\nwhen working with _SQLite_. Please, make sure to use _PostgreSQL_ to avoid problems.\n\n---\n\nIt expects the following parameters:\n\n- `klass` -> The model whose values will be retrieved or created\n- `values` -> A list of dictionaries having key value pairs demanded by `klass`\n- `only_create` -> A boolean value. Defaults to `False`. In case you don't care about getting\nexisting values, and just wants to create them, then you can set this arguments to `True`. It\nwill result in just one database hit.\n- `kwargs` -> Key value pairs with extra fields you want to use for filtering/creating instances\nof `klass`. It can be useful for foreign key fields\n\nUsage example:\n\n```python\n# serializers.py\n\nfrom awesome_tools.helpers import bulk_get_or_create, set_and_destroy\n\n\nclass MovieSerializer(serializers.ModelSerializer):\n    # ...\n\n    def create(self, validated_data: dict) -> Movie:\n        # ...\n\n        videos_data = validated_data.pop(\"videos\")\n\n        # ...\n\n        bulk_get_or_create(Video, videos_data, movie=movie)\n\n        # ...\n\n    def update(self, instance: Movie, validated_data: dict) -> Movie:\n        # ...\n\n        videos = validated_data.pop(\"videos\", None)\n\n        # ...\n\n        if videos:\n            set_and_destroy(\n                klass=instance,\n                attr=\"videos\",\n                value=bulk_get_or_create(Video, videos, movie=instance),\n                related_klass=Video,\n                movie=None,\n            )\n\n        # ...\n```\n\nNote that in the `update` method, we are combining `set_and_destroy` with `bulk_get_or_create`.\nThat's totally a thing.\n\nI highly encourage you to have a look at the source code, so that you can better understand what's\nhappening under the hood. It's not complicated.\n\n---\n\n## mixins\n\nThis module provides useful mixins to be used in Django Rest Framework **generic views** and **viewsets**.\n\n### SerializerByMethodMixin\n\nThis mixin overrides the `get_serializer_class` method of generic views. It's\npurpose is to dinamically define which serializer to use, depending on the request\nmethod. For this to be possible, a new class property should be set, it is:\n\n- `method_serializers` -> It should be a dictionary having it's keys with the names\nof http methods and values as the serializer classes corresponding to each method.\nIf the request method does not match any of the dict keys, it will return the value\nof `self.serializer_class`.\n\nBelow is an example:\n\n```python\n# views.py\n\nfrom awesome_tools.mixins import SerializerByMethodMixin\n\n\nclass MyBeautifulGenericView(SerializerByMethodMixin, ListCreateAPIView):\n    queryset = MyWonderfulModel.objects.all()\n    serializer_class = MyDefaultSerializer\n    method_serializers = {\n        \"GET\": MySerialzerToUseInGetRequests,\n    }\n```\n\n##\n\n### SerializerByActionMixin\n\nThis mixin overrides the `get_serializer_class` method of viewsets. It's\npurpose is to dinamically define which serializer to use, depending on the viewset\naction. For this to be possible, a new class property should be set, it is:\n\n- `action_serializers` -> It should be a dictionary having it's keys with the names\nof viewset actions and values as the serializer classes corresponding to each action.\nIf the viewset action does not match any of the dict keys, it will return the value\nof `self.serializer_class`.\n\nBelow is an example:\n\n```python\n# views.py\n\nfrom awesome_tools.mixins import SerializerByActionMixin\n\n\nclass MyBeautifulViewSet(SerializerByActionMixin, ModelViewSet):\n    queryset = MyWonderfulModel.objects.all()\n    serializer_class = MyDefaultSerializer\n    action_serializers = {\n        \"create\": MySerializerToUseInCreateActions,\n        \"update\": MySerialzerToUseInUpdateActions,\n        \"partial_update\": MySerialzerToUseInPartialUpdateActions,\n    }\n```\n\n##\n\n### SerializerByDetailActionsMixin\n\nThis mixin overrides the `get_serializer_class` method of viewsets. It's\npurpose is to dinamically define which serializer to use, depending on the viewset\naction. If it is a detail action, that is, one of `retrieve`, `update`, `partial_update`\nand `destroy`, then `self.detail_serializer_class` will be returned. Else, the default\n`self.serializer_class` is used. For this to be possible, a new class property should\nbe set, it is:\n\n- `detail_serializer_class` -> It's value should be a serializer class. This property defines\nwhich serializer to use in detail actions.\n\nBelow is an example:\n\n```python\n# views.py\n\nfrom awesome_tools.mixins import SerializerByDetailActionsMixin\n\n\nclass MyBeautifulViewSet(SerializerByDetailActionsMixin, ModelViewSet):\n    queryset = MyWonderfulModel.objects.all()\n    serializer_class = MyDefaultSerializer\n    detail_serializer_class = MyDetailSerializer\n```\n\n##\n\n### SerializerBySafeActionsMixin\n\nThis mixin overrides the `get_serializer_class` method of viewsets. It's\npurpose is to dinamically define which serializer to use, depending on the viewset\naction. If it is a _safe action_, then `self.safe_serializer_class` will be returned.\nElse, the default `self.serializer_class` is returned. A safe action is an action\nlisted in the `safe_actions` class property. For this to be possible, a new class\nproperty should be set, it is:\n\n- `safe_serializer_class` -> Its value should be a serializer class. This property defines\nwhich serializer to use in safe actions.\n\nYou can totally customize what is a \"safe action\". For that, you could change the value\nof `self.safe_actions`.\n\n- `safe_actions` -> It should be a `list[str]`, which each item representing a viewset action,\nconsidered safe for that viewset. The default value is `[\"list\", \"retrieve\"]`\n\nBelow is an example:\n\n```python\n# views.py\n\nfrom awesome_tools.mixins import SerializerBySafeActionsMixin\n\n\nclass MyBeautifulViewSet(SerializerBySafeActionsMixin, ModelViewSet):\n    queryset = MyWonderfulModel.objects.all()\n    serializer_class = MyDefaultSerializer\n    safe_serializer_class = MySafeSerializer\n```\n\n##\n\n### FilterQuerysetMixin\n\nThis mixin overrides the `get_queryset` method of class based views. It's main goal is\nto make it easier and simpler to filter and/or narrow down results. You may use it to\nattach results to the logged in user, to filter the queryset by route params (or `kwargs`)\nand by query params.\n\nThese are the class properties that this mixin accepts:\n\n- `filter_user_key` -> A `str` representing which keyword argument should be used for filtering by\nuser. The default is `None`, meaning that the queryset will not be filtered by the logged in user, that\nis, `self.request.user`. If in your queryset there is a `FK` pointing to your project's auth user model, then this property should\nhave the same name as this `FK` field.\n- `filter_kwargs` -> A `dict[str, str]`, where the **key** represents the name of the **field** to be searched,\nand the **value** is the **url param**.\n- `filter_query_params` -> A `dict[str, str]`, where the **key** is the name of the **field** to be searched,\nand the **value** represents the **query param** received in the request.\n- `filter_exception_klass` -> Should be an `exception` inheriting from `rest_framework.exceptions.APIException`. The\ndefault value is `django.http.Http404`. In case no value is returned or another kind of error occurs, this\nexception will be raised.\n- `filter_accept_empty` -> A `bool`, which defaults to `True`. If `False`, then the `exception_klass` will be raised\nin case the results are empty. Otherwise, an empty value will be returned normaly.\n\nBelow is an example of how this might be useful:\n\n```python\n# request endpoint\n\n\"/categories/<category_id>/transactions/\"\n\n```\n\n```python\n# views.py\n\nfrom awesome_tools.mixins import FilterQuerysetMixin\n\nclass TransactionView(FilterQuerysetMixin, ListCreateAPIView):\n    serializer_class = TransactionSerializer\n    permission_classes = [IsAuthenticated]\n    filter_user_key = \"user\"\n    filter_kwargs = {\"category\": \"category_id\"}\n    filter_query_params = {\n        \"month_id\": \"month_id\",\n        \"month__number\": \"month_number\",\n        \"month__name__icontains\": \"month_name\",\n        \"month__year\": \"year\",\n        \"description__icontains\": \"description\",\n        \"value\": \"value\",\n        \"value__gte\": \"value_gte\",\n        \"value__lte\": \"value_lte\",\n        \"is_income\": \"is_income\",\n        \"is_recurrent\": \"is_recurrent\",\n        \"installments\": \"installments\",\n    }\n    filter_order_by = [\"-created_at\"]\n```\n\nIn the example above, we are defining a view for monetary transactions. We don't want\nusers to see other user's transactions, so we attach all transactions to the logged in\nuser. By using the `filter_user_key` class property, we tell the mixin that when filtering the\nqueryset, it should use `user=self.request.user`.\n\nAlso, all transactions have categories. And we want them always to be listed by category.\nSo in the url, we receive the `<category_id>` param. So that's why we declare `filter_kwargs`\nin that way.\n\nAs for the `filter_query_params` property, please note how interesting it is. In the **keys** of\nthe dictionary, we pass in the keys that will be used for filtering the queryset, just as if\nwe were filtering the queryset manually. And the **values** correspond to the query param that we\nexpect to receive in the request. None of these query params are mandatory.\n\nWe are not declaring `filter_accept_empty`, which means that we will not raise `filter_exception_klass`\nin any case (because the default value is `True`). So that's why we don't need to define `filter_exception_klass` too.\n\nFurthermore, we are ordering the queryset results by their creation date (in descending order) with the\n`filter_order_by` property.\n\nYou may have noticed that the `queryset` class property haven't been defined. That's not a\nproblem, because this mixin guesses what is the apropriated model by accessing `self.serializer_class.Meta.model`.\nSo as long as you define you model in that way, everything is OK.\n\n##\n\n### AttachUserOnCreateMixin\n\nThis mixin overrides the `perform_create` method of generic views, and simply passes to the serializer\n`save` method an additional keyword argument. This attaches the current user to the `validated_data`\nargument on the serializer's `create` method. You can pass the following class property:\n\n`attach_user_key` -> A `str`, which defaults to `None`. It represents which is the name of the field\nthat points to the user on your model. If ommited, it will try to get the value of `self.filter_user_key`.\n\nSo in case you are already using this module's `FilterQuerysetMixin`, and is using this property, then there\nis no need to repeat yourself here. But in case neither `self.attach_user_key` or `self.filter_user_key` are\nfound, then `\"user\"` is used by default.\n    \nHere is a quick example:\n\n```python\n\nfrom awesome_tools.mixins import AttachUserOnUpdateMixin\nfrom rest_framework import generics\nfrom rest_framework import permissions\n\nfrom .serializers import CinemaSerializer\n\nclass CinemaView(AttachUserOnUpdateMixin, generics.ListCreateAPIView):\n    serializer_class = CinemaSerializer\n    permission_classes = [permissions.IsAuthenticated]\n    attach_user_key = \"owner\"\n\n```\n\nThis simple trick makes it possible to attach an user to a `Cinema` instance very easily. In this case, we are\ndefining `attach_user_key` as `\"owner\"`, because on the `Cinema` model, the foreign key field that relates to\nthe user model, has this name.\n\n##\n\n### AttachUserOnUpdateMixin\n\nExactly the same as [AttachUserOnCreateMixin](#attachuseroncreatemixin), but overrides the `perform_update`\nmethod.\n\n##\n\n### AttachUserToReqDataMixin\n\nA combination of [AttachUserOnCreateMixin](#attachuseroncreatemixin) and [AttachUserOnUpdateMixin](#attachuseronupdatemixin), \noverriding both `perform_create` and `perform_update` methods of generic views.\n\n\n---\n\n## managers\n\nThis module provides a custom user manager as a shortcut for whoever wants to customize\ndjango's authentication system to use a different field instead of username for login.\nIt can be really anything, like email, phone, cpf, etc.\n\n### CustomUserManager\n\nA custom user manager that inherits from `django.contrib.auth.models.BaseUserManager`.\nIts purpouse in life is mainly to provide an easy and simple way to implement a login\nand register system that expects another field instead of `username`.\n\nBut what if you desired to customize your users in a way that other info is also required\nfor user creation? No problem, this class is highly customizable.\n\nInstead of having to override the `create` and `create_superuser` methods of `BaseUserManager`,\nyou can inherit from `CustomUserManager` and then simply set some class properties at your will.\nThey work as follows:\n\n- `auth_field_name` -> Defaults to `\"email\"`. Defines what is the name of the field that\nshould be used for login (besides password, of course). Note that this field **must**\nexist in your user model, **having a unique constraint**.\n- `user_is_staff` -> Defaults to `False`. Defines the starting staff status of newly\ncreated users\n- `user_start_active` -> Defaults to `True`. Defines if a user account should start in\nactive state. In cases where users have to confirm their account in some way before getting\naccess, you may wish to set this property to `False`\n- `super_is_staff` -> Defaults to `True`. Defines the starting staff status of newly\ncreated superusers\n- `super_start_active` -> Defaults to `True`. Defines if a superuser account should start in\nactive state. Usually you'll want this value to be `True`, but you're totally free to change\nit, depending on your needs.\n- `required_fields` -> Defaults to `[]`. It should be a `list[str]`. This property defines\nwhich fields are required to be provided upon user creation, besides `self.auth_field_name` and\n`password`. The fields `is_staff`, `is_superuser` and `is_active` should also not be present in\nthis list. It is worth noting that **all fields defined here, must also be defined in your user model**.\nOtherwise, a `ValidationError` is raised.\n\nBelow is an example of how you may customize the behaviour of this class:\n\n```python\n# managers.py\n\nfrom awesome_tools.managers import CustomUserManager\n\n\nclass MyOwnUserManager(CustomUserManager):\n    user_start_active = False\n    required_fields = [\"first_name\", \"last_name\"]\n```\n\nIn order to implement a login with email feature, for instance, you have to make some minor\nchanges to your user model. Below are some settings that may come in handy for you to define\nin your model:\n\n```python\n# models.py\n\nfrom .managers import MyOwnUserManager\nfrom django.db import models\nfrom django.contrib.auth.models import AbstractUser\n\n\nclass MyUser(AbstractUser):\n    email = models.EmailField(unique=True)\n\n    username = None\n\n    objects = MyOwnUserManager()\n\n    USERNAME_FIELD = objects.auth_field_name\n    REQUIRED_FIELDS = objects.required_fields\n```\n\nThe `email` property is defined as unique, since it's gonna be used for login (as per the `USERNAME_FIELD`\nproperty). The `objects` property may be either the standard `awesome_tools.managers.CustomUserManager`\nor your own manager that inherits from it. In the example above, we are using our own user manager,\nwith some minor customizations. `REQUIRED_FIELDS` refer to the fields you are prompted when creating a\nsuperuser (it must not include the value defined for `USERNAME_FIELD` or `\"password\"`). Defining it to\n`objects.required_fields` prevents you from making mistakes and being redundant. Note that in the example\nabove we are droping the `username` column, but that's not necessary if you still want to have a username\nin your user model.\n\n---\n\n## cache\n\nThis subpackage provides a set of useful mixins that may be used for cache management. It also provides a function\nfor building your own custom mixins.\n\n### build_cache_mixins\n\nThis function returns a tuple of mixins used for cache management. It receives the following arguments:\n    \n- `cache_ttl` -> The ttl(time to live) for the cache is by default whatever you\nset in the `CACHE_TTL` variable at your project's `settings.py`, but you can totally override this here.\nThe value of `cache_ttl` must be an `int`. It represents the time that the cache will persist, **in seconds**.\nIn case `CACHE_TTL` is not present in `settings.py`, then it defaults to 10 minutes.\n- `vary_on_headers` -> This argument is a `tuple` that refers to which headers should be used when generating\nthe cache key and cache group.\n- `vary_on_user` -> A boolean value that determines if the cache key and cache group should be isolated for each user.\n\nIt is important to note that, besides the value of `vary_on_headers` and `vary_on_user`, cache keys are generated\nbased on the request path and query params, and cache groups are generated based on the request path.\n\nHere is a simple example of how you could use it:\n\n```python\n\nfrom awesome_tools.cache import build_cache_mixins\n\n(\n    SetCacheOnListByMyCoolHeaderMixin,\n    EraseCacheOnCreateByMyCoolHeaderMixin,\n    EraseCacheOnUpdateByMyCoolHeaderMixin,\n    EraseCacheOnDestroyByMyCoolHeaderMixin,\n    EraseCacheOnDetailByMyCoolHeaderMixin,\n    ManageCacheByMyCoolHeaderMixin,\n    FullManageCacheByMyCoolHeaderMixin,\n) = build_cache_mixins(vary_on_headers=(\"my-cool-header\",))\n\n```\n\n---\n\nIn the example above, if the same request is made again on a cached view, but with a different value on `\"my-cool-header\"`\nheader, then the view will not use the cached value, rather, it will cache the results also based on this header.\n\nActually, this illustrates exactly how the cache management mixins on this package are generated.\n\n##\n\n### SetCacheOnListMixin\n\nCaches the results of the `list` method of generic views and viewsets. After setting the cache,\nif the same request is fired again, then it will return the cached value, instead of doing the\nwhole thing again.\n\nHere is a simple example of how you could use this mixin.\n\n```python\n\nfrom awesome_tools.cache import SetCacheOnListMixin\nfrom rest_framework.generics import ListAPIView\n\n\nclass MyAwesomeListView(SetCacheOnListMixin, ListAPIView):\n    # my awesome view stuff\n    ...\n\n```\n\n##\n\n### EraseCacheOnCreateMixin\n\nUpon calling the `create` method of generic views and viewsets, erase the cache. But what cache?\nThe cache related to the group this mixin belongs to. It is by default determined by the url path,\nbut may vary based on user or any headers on the request, if these arguments are passed to the\n`build_cache_mixins` function.\n\nHere is an example:\n\n```python\n\nfrom awesome_tools.cache import SetCacheOnListMixin, EraseCacheOnCreateMixin\nfrom rest_framework.generics import ListAPIView, CreateAPIView\n\n\nclass MyAwesomeListView(SetCacheOnListMixin, ListAPIView):\n    # my awesome view stuff\n    ...\n\nclass MyAwesomeCreateView(EraseCacheOnCreateMixin, CreateAPIView):\n    # my awesome view stuff\n    ...\n\n```\n\nIn the example above, when the create view is called, then it will erase any cache keys that were set\nby the list view, if there are any, but **only within the scope of the cache group**.\n\n##\n\n### EraseCacheOnUpdateMixin\n\nExactly the same as [EraseCacheOnCreateMixin](#erasecacheoncreatemixin), but with `update` and `partial_update`\nmethods of generic views and viewsets.\n\n##\n\n### EraseCacheOnDestroyMixin\n\nExactly the same as [EraseCacheOnCreateMixin](#erasecacheoncreatemixin) and [EraseCacheOnUpdateMixin](#erasecacheonupdatemixin),\nbut with the `destroy` method of generic views and viewsets.\n\n##\n\n### EraseCacheOnDetailMixin\n\nThis is just a combination of both [EraseCacheOnUpdateMixin](#erasecacheonupdatemixin) and [EraseCacheOnDestroyMixin](#erasecacheondestroymixin).\n\nHere is an example:\n        \n```python\n\nfrom awesome_tools.cache import SetCacheOnListMixin, EraseCacheOnDetailMixin\nfrom rest_framework.generics import ListAPIView, RetrieveUpdateDestroyAPIView\n\n\nclass MyAwesomeListView(SetCacheOnListMixin, ListAPIView):\n    # my awesome view stuff\n    ...\n\nclass MyAwesomeDetailView(EraseCacheOnDetailMixin, RetrieveUpdateDestroyAPIView):\n    # my awesome view stuff\n    ...\n\n```\n\nIn the example above, when the detail view is called, then it will erase any cache keys that were set\nby the list view, if there are any, but **only within the scope of the cache group**.\n\n##\n\n### ManageCacheMixin\n\nUpon calling the `list` method of the view, set the cache, but when calling the `create` method, then erase the cache.\nThis mixin is essentially just a combination of both [SetCacheOnListMixin](#setcacheonlistmixin) and [EraseCacheOnCreateMixin](#erasecacheoncreatemixin).\n\nHere is an example:\n\n```python\n\nfrom awesome_tools.cache import ManageCacheMixin\nfrom rest_framework.generics import ListCreateAPIView\n\n\nclass MyAwesomeView(ManageCacheMixin, ListCreateAPIView):\n    # my awesome view stuff\n    ...\n\n```\n\nIn the example above, when the `list` method of the view is called, then it will set the cache, but when the `create`\nmethod is called, then it will erase any cache keys that were set on the `list` method, if there are any, but\n**only within the scope of the cache group**. The cache group is by default determined by the url path, but may vary\nbased on user or any headers on the request, if these arguments are passed to the `build_cache_mixins` function.\n\n##\n\n### FullManageCacheMixin\n\nUpon calling the `list` method of the view, set the cache, but when calling `create`, `update`, `partial_update` and\n`destroy` methods, then erase the cache. This mixin is essentially just a combination of both [ManageCacheMixin](#managecachemixin)\nand [EraseCacheOnDetailMixin](#erasecacheondetailmixin).\n\nHere is an example:\n\n```python\n\nfrom awesome_tools.cache import FullManageCacheMixin\nfrom rest_framework.viewsets import ModelViewSet\n\n\nclass MyAwesomeViewSet(FullManageCacheMixin, ModelViewSet):\n    # my awesome viewset stuff\n    ...\n\n```\n\nIn the example above, when the `list` method of the viewset is called, then it will set the cache, but when any of\n`create`, `update`, `partial_update` and `destroy` methods is called, then it will erase any cache keys that were\nset on the `list` method, if there are any, but **only within the scope of the cache group**. The cache group is\nby default determined by the url path, but may vary based on user or any headers on the request, if these arguments\nare passed to the `build_cache_mixins` function.\n\n##\n\n### ByAuthToken Variations\n\n### SetCacheOnListByAuthTokenMixin\n\nExactly the same as [SetCacheOnListMixin](#setcacheonlistmixin), but grouping the cache by the `\"Authorization\"` header.\nThis means that the cache will be isolated by the value of the auth token, even within the scope of the same user.\n\nThe following mixins are all variations of the mixins described until now, just like the one above. They all group the\ncache by `\"Authorization\"` header. They are as follows:\n\n- ### EraseCacheOnCreateByAuthTokenMixin\n- ### EraseCacheOnUpdateByAuthTokenMixin\n- ### EraseCacheOnDestroyByAuthTokenMixin\n- ### EraseCacheOnDetailByAuthTokenMixin\n- ### ManageCacheByAuthTokenMixin\n- ### FullManageCacheByAuthTokenMixin\n\n##\n\n### ByUser Variations\n\n### SetCacheOnListByUserMixin\n\nExactly the same as [SetCacheOnListMixin](#setcacheonlistmixin), but grouping the cache by `request.user`. This means that\nthe cache will be isolated by user, even if the authorization token may expire.\n\nThe following mixins are all variations of the mixins described until now, just like the one above. They all group the\ncache by `request.user`. They are as follows:\n\n- ### EraseCacheOnCreateByUserMixin\n- ### EraseCacheOnUpdateByUserMixin\n- ### EraseCacheOnDestroyByUserMixin\n- ### EraseCacheOnDetailByUserMixin\n- ### ManageCacheByUserMixin\n- ### FullManageCacheByUserMixin\n\n---\n\n## docs\n\nThis module provides a set of useful and simple to use functions for building documentation mixins.\nBut wait, what is a \"documentation mixin\"? The `drf-spectacular` package has a wonderful feature of\nautomatically generating a swagger documentation, based on your project views, with minimum configurations\non `settings.py` and `urls.py`. But maybe you want to give more details on how each view and endpoint works,\nsuch as a brief summary, a detailed description, or maybe specify which query parameters it accepts. For that,\n`drf-spectacular` library provides some ways for us to apply such customizations.\n\nAmong them, is the `drf_spectacular.utils` module. It has a decorator called `extend_schema`, which can be used\nto decorate view methods that correspond to http methods, like `get`, `post` and so forth. It accepts many arguments.\n\nThe functions defined in this module use this decorator to build mixins that can be easily aplied to any class based view.\nIt is recommended to put these mixins on the far left of your views inheritance list, as show in the examples below.\n\n### build_list_docs\n\nCreate a mixin class that applies the `drf_spectacular.utils.extend_schema` decorator to the\n`get` method of views. It receives the following arguments:\n\n`summary` -> A `str`, which is a brief description of what that endpoint does.\n`description` -> Also a `str`, which can be used to provide further details on the behavior of the\nendpoint.\n`parameters` -> A `list` filled with values of type `OpenApiParameter`.\n`*args, **kwargs` -> Any other parameters that `@extend_schema` expects.\n\nHere's an example:\n\n```python\nfrom awesome_tools.docs import build_list_docs\nfrom drf_spectacular.utils import OpenApiParameter\n\nsummary = \"A wonderful and brief description of the list action on an endpoint\"\n\ndescription = (\n    \"A more detailed description of the list action and some tips on how to use it. \"\n    \"Here I could add anything more that I may wish to appear on swagger.\"\n)\n\nparameters = [\n    OpenApiParameter(\"param1\", int, description=\"A brief description of my int query param\"),\n    OpenApiParameter(\"param2\", bool, description=\"A brief description of my bool query param\"),\n]\n\nMyListViewDocsMixin = build_list_docs(summary, description, parameters)\n```\n\n```python\n# views.py\n\nclass MyListView(MyListViewDocsMixin, ListAPIView):\n    # view stuff\n    ...\n```\n\n##\n\n### build_create_docs\n\nSame as above, but with the `post` http method, for create views\n\n##\n\n### build_retrieve_docs\n\nSame as above, but with the `get` http method, for retrieve views\n\n##\n\n### build_update_docs\n\nSame as above, but with the `put` and `patch` http methods, for update views\n\n##\n\n### build_destroy_docs\n\nSame as above, but with the `destroy` http method, for destroy views\n\n##\n\n### build_list_create_docs\n\nJust a combination of `build_list_docs` and `build_create_docs`. Returns a single mixin, that combine\nboth `ListDocsMixin` and `CreateDocsMixin`, which are the return values of these two functions, respectively.\nIt may come in handy for `ListCreate` views. It receives the following arguments:\n\n`summaries` -> A `dict`, containing the summaries of each endpoint, where the keys are any of `[\"list\", \"create\"]`, \nand the values are the summaries.\n`descriptions` -> A `dict`, containing the desciptions of each endpoint, where the keys are any of `[\"list\", \"create\"]`, \nand the values are the desciptions.\n`parameters` -> A `list` filled with values of type `OpenApiParameter`, to be used on the `list` endpoint.\n`*args, **kwargs` -> Any other parameters that `@extend_schema` expects (note that this will be applied to both methods).\n\nHere's an example:\n\n```python\nfrom awesome_tools.docs import build_list_create_docs\nfrom drf_spectacular.utils import OpenApiParameter\n\nsummaries = {\n    \"list\": \"A wonderful and brief description of the list action on an endpoint\",\n    \"create\": \"A wonderful and brief description of the create action on an endpoint\",\n}\n\ndescriptions = {\n    \"create\": (\n        \"A more detailed description of the create action and some tips on how to use it. \"\n        \"Here I could add anything more that I may wish to appear on swagger.\"\n    )\n}\n\nparameters = [\n    OpenApiParameter(\"param1\", int, description=\"A brief description of my int query param\"),\n    OpenApiParameter(\"param2\", bool, description=\"A brief description of my bool query param\"),\n]\n\nMyViewDocsMixin = build_list_create_docs(summaries, descriptions, parameters)\n```\n\n```python\n# views.py\n\nclass MyView(MyViewDocsMixin, ListCreateAPIView):\n    # view stuff\n    ...\n```\n\n##\n\n### build_retrieve_update_destroy_docs\n\nJust a combination of `build_retrieve_docs` and `build_update_docs` and `build_destroy_docs`. Returns a single\nmixin, that combine `RetrieveDocsMixin`, `UpdateDocsMixin` and `DestroyDocsMixin`, which are the return values\nof these three functions, respectively. It may come in handy for `RetrieveUpdateDestroy` views. It receives the\nfollowing arguments:\n\n`summaries` -> A `dict`, containing the summaries of each endpoint, where the keys are any of `[\"retrieve\", \"update\", \"destroy\"]`, \nand the values are the summaries.\n`descriptions` -> A `dict`, containing the desciptions of each endpoint, where the keys are any of `[\"retrieve\", \"update\", \"destroy\"]`, \nand the values are the desciptions.\n`*args, **kwargs` -> Any other parameters that `@extend_schema` expects (note that this will be applied to all methods).\n\nHere's an example:\n\n```python\nfrom awesome_tools.docs import build_retrieve_update_destroy_docs\n\nsummaries = {\n    \"retrieve\": \"A wonderful and brief description of the retrieve action on an endpoint\",\n    \"update\": \"A wonderful and brief description of the update action on an endpoint\",\n    \"destroy\": \"A wonderful and brief description of the delete action on an endpoint\",\n}\n\ndescriptions = {\n    \"destroy\": (\n        \"A long description of the destroy action and a warning about its consequences\"\n        \"Here I could add anything more that I may wish to appear on swagger.\"\n    )\n}\n\nMyDetailViewDocsMixin = build_retrieve_update_destroy_docs(summaries, descriptions)\n```\n\n```python\n# views.py\n\nclass MyDetailView(MyDetailViewDocsMixin, RetrieveUpdateDestroyAPIView):\n    # view stuff\n    ...\n```\n\n##\n\n### build_docs\n\nReturn a tuple of five mixins, each of which are built using `build_list_docs`, `build_create_docs`\n`build_retrieve_docs`, `build_update_docs` and `build_destroy_docs`, respectively. It receives the following arguments:\n\n`summaries` -> A `dict`, containing the summaries of each endpoint, where the keys are any of\n`[\"list\", \"create\", \"retrieve\", \"update\", \"destroy\"]`, and the values are the summaries.\n`descriptions` -> A `dict`, containing the desciptions of each endpoint, where the keys are any of\n`[\"list\", \"create\", \"retrieve\", \"update\", \"destroy\"]`, and the values are the desciptions.\n`parameters` -> A `list` filled with values of type `OpenApiParameter`, to be used on the `list` endpoint.\n`*args, **kwargs` -> Any other parameters that `@extend_schema` expects (note that this will be applied to all methods).\n\nHere's an example:\n\n```python\nfrom awesome_tools.docs import build_docs\nfrom drf_spectacular.utils import OpenApiParameter\n\nsummaries = {\n    \"list\": \"A wonderful and brief description of the list action on an endpoint\",\n    \"create\": \"A wonderful and brief description of the create action on an endpoint\",\n    \"retrieve\": \"A wonderful and brief description of the retrieve action on an endpoint\",\n    \"update\": \"A wonderful and brief description of the update action on an endpoint\",\n    \"destroy\": \"A wonderful and brief description of the delete action on an endpoint\",\n}\n\ndescriptions = {\n    \"destroy\": (\n        \"A long description of the destroy action and a warning about its consequences\"\n        \"Here I could add anything more that I may wish to appear on swagger.\"\n    )\n}\n\nparameters = [\n    OpenApiParameter(\"param1\", int, description=\"A brief description of my int query param\"),\n    OpenApiParameter(\"param2\", bool, description=\"A brief description of my bool query param\"),\n]\n\n(\n    MyListViewDocsMixin,\n    MyCreateViewDocsMixin,\n    MyRetrieveViewDocsMixin\n    MyUpdateViewDocsMixin,\n    MyDestroyViewDocsMixin,\n) = build_docs(summaries, descriptions, parameters)\n```\n\n```python\n# views.py\n\nclass MyListView(MyListViewDocsMixin, ListAPIView):\n    # view stuff\n    ...\n\nclass MyCreateView(MyCreateViewDocsMixin, CreateAPIView):\n    # view stuff\n    ...\n\n# and so forth...\n```\n\n##\n\n### build_docs_by_group\n\nReturn a tuple with two mixins, which of which are built using `build_list_create_docs` and\n`build_retrieve_update_destroy_docs`, respectively. This may come in handy in those cases where\nyou have two classes, one is a `ListCreateView` and the other is a `RetrieveUpdateDestroyView`.\nIt receives the following arguments:\n\n`summaries` -> A `dict`, containing the summaries of each endpoint, where the keys are any of\n`[\"list\", \"create\", \"retrieve\", \"update\", \"destroy\"]`, and the values are the summaries.\n`descriptions` -> A `dict`, containing the desciptions of each endpoint, where the keys are any of\n`[\"list\", \"create\", \"retrieve\", \"update\", \"destroy\"]`, and the values are the desciptions.\n`parameters` -> A `list` filled with values of type `OpenApiParameter`, to be used on the `list` endpoint.\n`*args, **kwargs` -> Any other parameters that `@extend_schema` expects (note that this will be applied to all methods).\n\nHere's an example:\n\n```python\nfrom awesome_tools.docs import build_docs_by_group\nfrom drf_spectacular.utils import OpenApiParameter\n\nsummaries = {\n    \"list\": \"A wonderful and brief description of the list action on an endpoint\",\n    \"create\": \"A wonderful and brief description of the create action on an endpoint\",\n    \"retrieve\": \"A wonderful and brief description of the retrieve action on an endpoint\",\n    \"update\": \"A wonderful and brief description of the update action on an endpoint\",\n    \"destroy\": \"A wonderful and brief description of the delete action on an endpoint\",\n}\n\ndescriptions = {\n    \"destroy\": (\n        \"A long description of the destroy action and a warning about its consequences\"\n        \"Here I could add anything more that I may wish to appear on swagger.\"\n    )\n}\n\nparameters = [\n    OpenApiParameter(\"param1\", int, description=\"A brief description of my int query param\"),\n    OpenApiParameter(\"param2\", bool, description=\"A brief description of my bool query param\"),\n]\n\n(\n    MyViewDocsMixin,\n    MyDetailViewDocsMixin,\n) = build_docs_by_group(summaries, descriptions, parameters)\n```\n\n```python\n# views.py\n\nclass MyView(MyViewDocsMixin, ListCreateAPIView):\n    # view stuff\n    ...\n```\n\n##\n\n### build_full_docs\n\nReturn a single mixin, that is a combination of both `ListCreateDocs` and `RetrieveUpdateDestroyDocs`,\nwhich `build_docs_by_group` returns. It receives the following arguments:\n\n`summaries` -> A `dict`, containing the summaries of each endpoint, where the keys are any of\n`[\"list\", \"create\", \"retrieve\", \"update\", \"destroy\"]`, and the values are the summaries.\n`descriptions` -> A `dict`, containing the desciptions of each endpoint, where the keys are any of\n`[\"list\", \"create\", \"retrieve\", \"update\", \"destroy\"]`, and the values are the desciptions.\n`parameters` -> A `list` filled with values of type `OpenApiParameter`, to be used on the `list` endpoint.\n`*args, **kwargs` -> Any other parameters that `@extend_schema` expects (note that this will be applied to all methods).\n\n    \nHere's an example:\n\n```python\nfrom awesome_tools.docs import build_full_docs\nfrom drf_spectacular.utils import OpenApiParameter\n\nsummaries = {\n    \"list\": \"A wonderful and brief description of the list action on an endpoint\",\n    \"create\": \"A wonderful and brief description of the create action on an endpoint\",\n    \"retrieve\": \"A wonderful and brief description of the retrieve action on an endpoint\",\n    \"update\": \"A wonderful and brief description of the update action on an endpoint\",\n    \"destroy\": \"A wonderful and brief description of the delete action on an endpoint\",\n}\n\ndescriptions = {\n    \"destroy\": (\n        \"A long description of the destroy action and a warning about its consequences\"\n        \"Here I could add anything more that I may wish to appear on swagger.\"\n    )\n}\n\nparameters = [\n    OpenApiParameter(\"param1\", int, description=\"A brief description of my int query param\"),\n    OpenApiParameter(\"param2\", bool, description=\"A brief description of my bool query param\"),\n]\n\nMyViewFullDocsMixin = build_full_docs(summaries, descriptions, parameters)\n\n```\n\n---\n\n## action_patterns\n\nViewsets have the advantage of abstracting away the work of defining routes explicitly,\nbut routers have some limits. They can only go to a certain depth in producing urls.\n\nFor instance, let's imagine a simple application, where you have Bands and Albums.\nIn case you wish to list all Albums of a Band, you could make a request to an enpoint\nlike `/bands/<band_id>/albums/`. That's totally possible with routers. But what if you\nwant a detail route for an Album of a Band? A route like `/bands/<band_id>/albums/<album_id>/`\nwould make sense, right? But routers aren't able to go to such an extent. And you could\ntotally imagine bigger urls in real, bigger applications.\n\nSo defining our routes manually gives us a lot more control. Everything comes with a tradeoff\nthough. When manually defining routes for generic views, you can easily assign each view class\nto their routes, using the `as_view` method. But viewsets are different. One viewset class can\nbe assigned to more than one route. So for that to work, you've gotta do something like [this](https://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/#binding-viewsets-to-urls-explicitly).\n\nIn order to simplify things, and abstract away some boiler plate code, this module provides the\nstandard viewset actions mapped to their corresponding http method. Of course, you may have additional\nactions, customized according to your own needs. In this case, you can config them on your own. But\nthe standard ones are all set here.\n\nUsage example:\n\n```python\n# urls.py\n\nfrom django.urls import path\nfrom awesome_tools.action_patterns import STANDARD_DETAIL_PATTERN, STANDARD_PATTERN\n\nfrom . import views\n\n\ncinema_view = views.CinemaViewSet.as_view(STANDARD_PATTERN)\ncinema_detail_view = views.CinemaViewSet.as_view(STANDARD_DETAIL_PATTERN)\n\nurlpatterns = [\n    path(\"\", cinema_view),\n    path(\"<cinema_id>/\", cinema_detail_view),\n]\n```\n\nBut routers are still so cool and so simple to use. So a very good alternative is [drf-nested-routers](https://github.com/alanjds/drf-nested-routers).\nIt really makes it easier to deal with all of this. The `drf-nested-routers` library is designed to\nsolve exactly this problem, and even more.\n\n---\n\n## admin\n\nThis module provides a `CustomUserAdmin` class. It inherits from `django.contrib.auth.admin.UserAdmin`.\nHave you ever created a custom user model, added it to admin and then realized that your users passwords\nwere being created unhashed? Then you searched the internet and found out that django provides a `UserAdmin`\nclass that does the job. But what if you customized your authentication system, and you're using another\nfield instead of `username`? In this case, it throws an error, saying that there is no `username` field.\n\nIn order to make things easier, this module provides a class that abstracts away all the boring \nconfigurations you would need to do.\n\n### CustomUserAdmin\n\nThis class inherits from `django.contrib.auth.admin.UserAdmin`. It's purpose in life is to abstract\naway some boring configurations you may need, when you're using a custom user model. The advantage is\nto have the same features that Django standard `UserAdmin` class provides, but in a custom user model,\nhaving a field other than `username` used for authentication.\n\nThis class automaticaly figures out what is your user model, as long as it is pointed to by `AUTH_USER_MODEL`\nsetting in `settings.py`. Also, it takes the care of first checking for the fields you set in your user\nmodel before referencing them. But the **password field is mandatory**.\n\nBelow is an usage example:\n\n```python\n# admin.py\n\nfrom awesome_tools.admin import CustomUserAdmin\nfrom .models import User\n\nadmin.site.register(User, CustomUserAdmin)\n```\n\nIn case you want to customize some kind of behaviour, you totally can, either by overwriting the properties\nentirely (by inheriting this class), or by using one of the class methods defined in this class. For instance,\nif you added some columns that are not default of auth user model, but still want them to appear in the admin,\nyou could do something like this:\n\n```python\n\n# admin.py\n\nfrom awesome_tools.admin import CustomUserAdmin\nfrom .models import User\n\nfields = (\"cpf\", \"phone\")\n\n# add fields to the user creation form\nCustomUserAdmin.add_creation_fields(fields)\n# append fields to list_display\nCustomUserAdmin.add_list_display(fields)\n# add fields to personal info screen\nCustomUserAdmin.add_personal_info(fields)\n\nadmin.site.register(User, CustomUserAdmin)\n```\n\nNot so bad.\n",
    "bugtrack_url": null,
    "license": "MIT License",
    "summary": "Awesome functions and classes for Django and Django Rest Framework",
    "version": "1.5.7",
    "split_keywords": [
        "django",
        "utils",
        "serializers",
        "generic",
        "views",
        "viewsets",
        "mixins",
        "email",
        "login",
        "model",
        "manager",
        "custom",
        "action",
        "shortcut",
        "error",
        "simple",
        "rest",
        "framework",
        "dj",
        "drf",
        "admin",
        "hash",
        "password",
        "user",
        "filter",
        "queryset",
        "cache",
        "management",
        "by auth token",
        "by user",
        "group",
        "key",
        "docs",
        "documentation",
        "spectacular",
        "customize",
        "swagger",
        "openapi"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "599e7753e0fa7f70dc1dec56cfd3ea05e64b5c85c9aa47cb076c36134852efe7",
                "md5": "e1addde36452b2f63f1dfddec801f4c6",
                "sha256": "d48fbae4d70e009d29874efd17f319317585b7363b66d5efc47d14b15b4ceb6f"
            },
            "downloads": -1,
            "filename": "django_awesome_tools-1.5.7-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "e1addde36452b2f63f1dfddec801f4c6",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 34480,
            "upload_time": "2023-04-05T11:13:23",
            "upload_time_iso_8601": "2023-04-05T11:13:23.359842Z",
            "url": "https://files.pythonhosted.org/packages/59/9e/7753e0fa7f70dc1dec56cfd3ea05e64b5c85c9aa47cb076c36134852efe7/django_awesome_tools-1.5.7-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "88677a59267fbd574fece672990bcbda99ab9b111299ca6338d44c9991a8ea68",
                "md5": "14b15440e0c04778c0617f707304c81d",
                "sha256": "269b15a9039033c9659294d8774e2a1eee5dba050703d6b7d32010060089c61a"
            },
            "downloads": -1,
            "filename": "django_awesome_tools-1.5.7.tar.gz",
            "has_sig": false,
            "md5_digest": "14b15440e0c04778c0617f707304c81d",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 52294,
            "upload_time": "2023-04-05T11:13:26",
            "upload_time_iso_8601": "2023-04-05T11:13:26.080935Z",
            "url": "https://files.pythonhosted.org/packages/88/67/7a59267fbd574fece672990bcbda99ab9b111299ca6338d44c9991a8ea68/django_awesome_tools-1.5.7.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-04-05 11:13:26",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "lcname": "django-awesome-tools"
}
        
Elapsed time: 0.05130s