Skip to main content
https://github.com/5monkeys/django-bananas/workflows/CI/badge.svg https://img.shields.io/pypi/v/django-bananas.svg

Install

django-bananas is on PyPI, so just run:

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:

python3 -m pip install django-bananas[drf]

Compatibility

Currently tested only for

  • Django 4.2 under Python 3.8-3.12

  • Django 5.0 under Python 3.10-3.13

  • Django 5.1 under Python 3.10-3.13

  • Django 5.2 under Python 3.10-3.14

  • Django 6.0 under Python 3.12-3.14

Pull requests welcome!

Examples

Models

TimeStampedModel

Abstract TimeStampedModel with date created/modified fields:

Use TimeStampedModel as base class for your model

from bananas.models import TimeStampedModel


class Book(TimeStampedModel):
    pass

the timestamps can be accessed on the model as

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

UUIDModel

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

from bananas.models import UUIDModel


class User(UUIDModel):
    display_name = models.CharField(max_length=255)
    email = models.EmailField()
>>> 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.

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)
>>> 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.

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)
>>> user.token
'WOgrNwqFKOF_LsHorJy_hGpPepjvVH7Uar-4Z_K6DzU-'

ORM

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

from bananas.query import ExtendedQuerySet


class Book(TimeStampedModel):
    author = ForeignKey(Author)
    objects = Manager.from_queryset(ExtendedQuerySet)()
>>> book = Book.objects.dicts("id", author="author__name").first()
{'id': 1, 'author': 'Jonas'}
>>> book.author
'Jonas'

Admin

Custom django admin stylesheet.

# 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',
}
# your main urls.py
from bananas import admin

urlpatterns = [
    # ...
    url(r"^admin/", include(admin.site.urls)),
]
# 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.

# 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 ...
# 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")),
]
# 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:

>>> from bananas.url import database_conf_from_url
>>> conf = database_conf_from_url(
...     "pgsql://joar:hunter2@5monkeys.se: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:
>>> # 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.:

>>> # 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.:

>>> # 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/.

>>> 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.

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.

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:

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 typehints on Python 3.14/Django 6.0 with:

python3 -m pip install -e .[test]
TOXENV=py314-django60,type-check 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.

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.

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:

make test test='-k test_logout'

Release files for django-bananas 3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-bananas 3.1
File Size Uploaded
django_bananas-3.1.tar.gz 51.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-bananas 3.1
File Interpreter ABI Platform
django_bananas-3.1-py3-none-any.whl Python 3 none any Details

Total release size: 106.8 kB

Release files / django_bananas-3.1.tar.gz

Download URL django_bananas-3.1.tar.gz
Size 51.6 kB
Tags Source
SHA-256 checksum
How to use checksums
d372f44b19e18e851877bf77e38939deae4a2705b3f7c782f62e9950e9721b7b
BLAKE2b-256 checksum
How to use checksums
effd14ab1c66596a6f55710e45090e2c22138e7f7b7032710ae1778ca54afaba
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 9, 2026.

Transparency log

Release files / django_bananas-3.1-py3-none-any.whl

Download URL django_bananas-3.1-py3-none-any.whl
Size 55.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7927d79e463b057652b634f0342af7ddef246728f333bb117c0bef5c035c8e15
BLAKE2b-256 checksum
How to use checksums
80bacf1a93818231709f0bab54081710c5af03f19a4e925be8c5ca93fa32cf2f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

3.1 This release

2 release files

3.0

2 release files

2.4

2 release files

2.3

2 release files

2.2

2 release files

2.1

2 release files

2.0

2 release files

1.5.5

2 release files

1.5.4

2 release files

1.5.3

2 release files

1.5.2

2 release files

1.5.1

2 release files

1.5

2 release files

1.4.4

1 release file

1.4.3

1 release file

1.4.2

1 release file

1.4.1

1 release file

1.4

1 release file

1.3.5

1 release file

1.3.4

1 release file

1.3.3

1 release file

1.3.2

1 release file

1.3.1

1 release file

1.3

1 release file

1.1

1 release file

1.0.8

2 release files

1.0.7

1.0.6

2 release files

1.0.5

1 release file

1.0.3

1 release file

1.0.2

1 release file

1.0.1

1 release file

1.0

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page