Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/5monkeys/django-bananas

Django extensions the monkey way
https://github.com/5monkeys/django-bananas

django python

Last synced: 4 days ago
JSON representation

Django extensions the monkey way

Awesome Lists containing this project

README

        

================================================================================
:banana: Django Bananas - Django extensions the monkey way
================================================================================

.. image:: https://github.com/5monkeys/django-bananas/workflows/CI/badge.svg
:target: https://github.com/5monkeys/django-bananas/actions

.. image:: https://img.shields.io/pypi/v/django-bananas.svg
:target: https://pypi.python.org/pypi/django-bananas/

--------------------------------------------------------------------------------
Install
--------------------------------------------------------------------------------

django-bananas is on PyPI, so just run:

.. code-block:: bash

python3 -m pip install django-bananas

Using DRF specific features like Bananas Admin and fencing requires
djangorestframework and drf-yasg and it's recommended to install django-bananas
with the ``drf`` extra to keep those in sync:

.. code-block:: bash

python3 -m pip install django-bananas[drf]

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Compatibility
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Currently tested only for

- Django 3.2 under Python 3.7-3.9
- Django 4.0 under Python 3.8-3.10
- Django 4.1 under Python 3.8-3.10
- Django 4.2 under Python 3.8-3.10

Pull requests welcome!

--------------------------------------------------------------------------------
Examples
--------------------------------------------------------------------------------

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Models
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

TimeStampedModel
================================================================================

Abstract ``TimeStampedModel`` with date created/modified fields:

Use TimeStampedModel as base class for your model

.. code-block:: py

from bananas.models import TimeStampedModel

class Book(TimeStampedModel):
pass

the timestamps can be accessed on the model as

.. code-block:: pycon

>>> book.date_created
>>> book.date_modified

UUIDModel
================================================================================

Abstract model that uses a Django 1.8 UUID field as the primary key.

.. code-block:: py

from bananas.models import UUIDModel

class User(UUIDModel):
display_name = models.CharField(max_length=255)
email = models.EmailField()

.. code-block:: pycon

>>> user.id
UUID('70cf1f46-2c79-4fc9-8cc8-523d67484182')

>>> user.pk
UUID('70cf1f46-2c79-4fc9-8cc8-523d67484182')

SecretField
================================================================================

Can be used to generate and store "safe" random bytes for authentication.

.. code-block:: py

from bananas.models import SecretField

class User(models.Model):
# Ask for 32 bytes and require 24 bytes from urandom
token = SecretField(num_bytes=32, min_bytes=24)

.. code-block:: pycon

>>> User.objects.create() # Token is generated automatically
>>> user.token
'3076f884da827809e80ced236e8da20fa36d0c27dd036bdd4afbac34807e5cf1'

URLSecretField
================================================================================

An implementation of SecretField that generates an URL-safe base64 string
instead of a hex representation of the random bytes.

.. code-block:: py

from bananas.models import URLSecretField

class User(models.Model):
# Generates an URL-safe base64 representation of the random value
token = URLSecretField(num_bytes=32, min_bytes=24)

.. code-block:: pycon

>>> user.token
'WOgrNwqFKOF_LsHorJy_hGpPepjvVH7Uar-4Z_K6DzU-'

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ORM
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

New ``queryset.dicts()`` with field renaming through kwargs, and `dot-dict`
style results:

.. code-block:: py

from bananas.query import ExtendedQuerySet

class Book(TimeStampedModel):
author = ForeignKey(Author)
objects = Manager.from_queryset(ExtendedQuerySet)()

.. code-block:: pycon

>>> book = Book.objects.dicts("id", author="author__name").first()
{'id': 1, 'author': 'Jonas'}
>>> book.author
'Jonas'

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Admin
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Custom django admin stylesheet.

.. warning:: Work in progress. Only a few views styled completely as of now.

.. code-block:: py

# settings.py
INSTALLED_APPS = (
"bananas", # Needs to be before "django.contrib.admin"
"django.contrib.admin",
...,
)

ADMIN = {
"SITE_HEADER": "Bananas",
"SITE_TITLE": "Bananas Admin",
"INDEX_TITLE": "Admin Panel",
# 'BACKGROUND_COLOR': '#363c3f',
}

.. code-block:: py

# your main urls.py
from bananas import admin

urlpatterns = [
# ...
url(r"^admin/", include(admin.site.urls)),
]

.. code-block:: py

# app/admin.py or something
from django.conf.urls import url
from bananas import admin

@admin.register
class MyAdminView(admin.AdminView):
def get_urls(self):
return [
url(r"^custom/$", self.admin_view(self.custom_view)),
# ^^ Note that the view is wrapped in self.admin_view.
# Needed for permissions and to prevent any
# threading issues.
]

def get(self, request):
return self.render("admin/template.html", {})

def custom_view(self, request):
return self.render("admin/custom.html", {})

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Admin API
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Django admin API for use with `django-bananas.js
`_ (react admin site). This
feature requires installation with the ``drf`` extra.

.. code-block:: py

# app/admin.py or something
from bananas.admin.api.mixins import BananasAPI
from bananas.admin.api.schemas import schema
from bananas.admin.api.views import BananasAdminAPI
from bananas.lazy import lazy_title
from django.utils.translation import gettext_lazy as _
from rest_framework import viewsets

class CustomAdminAPI(BananasAdminAPI):
name = lazy_title(_("custom"))

@schema(query_serializer=SomeSerializer, responses={200: SomeSerializer})
def list(self, request):
return ...

class SomeModelAdminAPI(BananasAPI, viewsets.ModelViewSet):
serializer_class = SomeModelSerializer

def list(self, request):
return ...

.. code-block:: py

# app/urls.py or something
from bananas.admin import api
from django.conf.urls import include, path

from .admin import CustomAdminAPI, SomeModelAdminAPI

api.register(CustomAdminAPI)
api.register(SomeModelAdminAPI)

urlpatterns = [
path(r"^api/", include("bananas.admin.api.urls")),
]

.. code-block:: py

# setting.py
ADMIN = {
"API": {
# Optional: override the default OpenAPI schemes
"SCHEMES": ["https"],
}
}

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Database URLs
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Parse database information from a URL, kind of like SQLAlchemy.

Engines
================================================================================

Currently supported engines are:

============================== ===========================================
URI scheme Engine
============================== ===========================================
pgsql, postgres, postgresql django.db.backends.postgresql_psycopg2
mysql django.db.backends.mysql
oracle django.db.backends.oracle
sqlite, sqlite3 django.db.backends.sqlite3
mysqlgis django.contrib.gis.db.backends.mysql
oraclegis django.contrib.gis.db.backends.oracle
postgis django.contrib.gis.db.backends.postgis
spatialite django.contrib.gis.db.backends.spatialite
============================== ===========================================

You can add your own by running ``register(scheme, module_name)`` before parsing.

database_conf_from_url(url)
Return a django-style database configuration based on ``url``.

:param url: Database URL
:return: Django-style database configuration dict

Example:

.. code-block:: pycon

>>> from bananas.url import database_conf_from_url
>>> conf = database_conf_from_url(
... "pgsql://joar:[email protected]:4242/tweets/tweetschema?hello=world"
... )
>>> sorted(conf.items()) # doctest: +NORMALIZE_WHITESPACE
[('ENGINE', 'django.db.backends.postgresql_psycopg2'),
('HOST', '5monkeys.se'),
('NAME', 'tweets'),
('PARAMS', {'hello': 'world'}),
('PASSWORD', 'hunter2'),
('PORT', 4242),
('SCHEMA', 'tweetschema'),
('USER', 'joar')]

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
bananas.environment - Helpers to get setting values from environment variables
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

``bananas.environment.env`` is a wrapper around ``os.environ``, it provides the
standard ``.get(key, value)``, method to get a value for a key, or a default if
the key is not set - by default that default is ``None`` as you would expect.
What is more useful is the additional type-parsing ``.get_*`` methods it
provides:

- ``get_bool``
- ``get_int``
- ``get_list``, ``get_set``, ``get_tuple``

:get_int:

.. code-block:: pycon

>>> # env ONE=1
>>> env.get_int("ONE")
1
>>> env.get_int("TWO") # Not set
None
>>> env.get_int("TWO", -1) # Not set, default to -1
-1

:get_bool:

returns ``True`` if the environment variable value is any of,
case-insensitive:

- ``"true"``
- ``"yes"``
- ``"on"``
- ``"1"``

returns ``False`` if the environment variable value is any of,
case-insensitive:

- ``"false"``
- ``"no"``
- ``"off"``
- ``"0"``

if the value is set to anything other than above, the default value will be returned instead.

e.g.:

.. code-block:: pycon

>>> # env CAN_DO=1 NO_THANKS=false NO_HABLA=f4lse
>>> env.get_bool("CAN_DO")
True
>>> env.get_bool("NO_THANKS")
False
>>> env.get_bool("NO_HABLA") # Set, but not valid
None
>>> env.get_bool("NO_HABLA", True) # Set, but not valid, with default
True
>>> env.get_bool("IS_NONE") # Not set
None
>>> env.get_bool("IS_NONE", False) # Not set, default provided
False

:get_tuple, get_list, get_set:

Returns a ``tuple``, ``list`` or ``set`` of the environment variable string,
split by the ascii comma character. e.g.:

.. code-block:: pycon

>>> # env FOOS=foo,foo,bar
>>> get_list("FOO")
['foo', 'foo', 'bar']
>>> get_set("FOO")
set(['foo', 'bar'])

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
bananas.secrets - Helpers for getting secrets from files
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Is useful for getting the content of secrets stored in files. One usecase is `docker secrets
`_.

``BANANAS_SECRETS_DIR`` can be used to configure the directory that secrets live in. Defaults to ``/run/secrets/``.

.. code-block:: pycon

>>> from bananas import secrets

>>> secrets.get_secret("hemlis")
"topsecret"

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
bananas.drf.fencing - Fence DRF views with HTTP conditional headers
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Building blocks for composing HTTP conditionals to guard DRF views. Built to
work well in conjunction with ``BananasAdminAPI`` and ``TimeStampedModel``. This
feature requires installation with the ``drf`` extra.

Fences add a header parameter to the exposed OpenAPI schema if you're using
drf-yasg.

``allow_if_unmodified_since``
=============================

Make a view-set for a ``TimeStampedModel`` only accept updates when
``If-Unmodified-Since`` specifies a date before the ``date_modified`` of the
updated instance.

Due to comparing datetime instances, using ``allow_if_unmodified_since``
requires running Django with timezone support enabled, ``USE_TZ = TRUE``.

.. code-block:: python

from bananas.drf.fencing import FencedUpdateModelMixin, allow_if_unmodified_since

class ItemAPI(FencedUpdateModelMixin, GenericViewSet):
fence = allow_if_unmodified_since()
serializer_class = ItemSerializer

``allow_if_match``
==================

Make a view-set that requires passing a version string in ``If-Match`` and
rejects requests when the given version does not match the ``version`` attribute
of the updated instance.

.. code-block:: python

from bananas.drf.fencing import FencedUpdateModelMixin, allow_if_match

class ItemAPI(FencedUpdateModelMixin, GenericViewSet):
fence = allow_if_match(operator.attrgetter("version"))
serializer_class = ItemSerializer

``Fence``
=========

Example implementing a fence for ``If-Modified-Since``:

.. code-block:: python

import operator
from drf_yasg import openapi
from rest_framework import status
from rest_framework.exceptions import APIException
from bananas.drf.fencing import Fence, header_date_parser, parse_date_modified

class NotModified(APIException):
status_code = status.HTTP_304_NOT_MODIFIED
default_detail = "An HTTP precondition failed"
default_code = "not_modified"

allow_if_not_modified_since = Fence(
get_token=header_date_parser("If-Modified-Since"),
compare=operator.gt,
get_version=parse_date_modified,
openapi_parameter=openapi.Parameter(
in_=openapi.IN_HEADER,
name="If-Modified-Since",
type=openapi.TYPE_STRING,
required=True,
description=(
"Time of last edit of the client's representation of the resource in "
"RFC7231 format."
),
),
rejection=NotModified("The resource is unmodified"),
)

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Contributing
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Contributing is welcome in the form of PRs and issues. If you want to add a
bigger feature or contribute with a large change in current behaviour it's
always a good idea to start a discussion with an issue before getting started.

New additions will be expected to have 100% test coverage as well as type hints
and documentation to be considered to be merged.

Development
===========

Testing and development requirements can be installed using package extras
``test`` and ``dev`` respectively. You'll most likely always want to install the
``drf`` extra when installing ``dev``.

To get started, setup a virtualenv and then install test requirements and run
tests and checks on Python 3.9/Django 3.1 with:

.. code-block:: bash

python3 -m pip install -e .[test]
TOXENV=py39-django31,checks python3 -m tox

You can install development requirements into your virtualenv. Linting and
formatting uses pre-commit which you could also install on a system level.

.. code-block:: bash

python3 -m pip install -e .[dev,drf]
make type-check
pre-commit run --all-files

After installing pre-commit, you can enable hooks to have it run before you
publish pull requests.

.. code-block:: bash

pre-commit install -t pre-push

After installing ``dev`` you can also run tests without tox for rapid iteration
and select specific tests with the ``test`` argument to ``make test``:

.. code-block:: bash

make test test='-k test_logout'