Package: django-bananas
Installdjango-bananas is on PyPI, so just run:python3-mpipinstalldjango-bananasUsing DRF specific features like Bananas Admin and fencing requires
djangorestframework and drf-yasg and it’s recommended to install django-bananas
with thedrfextra to keep those in sync:python3-mpipinstalldjango-bananas[drf]CompatibilityCurrently tested only forDjango 3.2 under Python 3.7-3.9Django 4.0 under Python 3.8-3.10Django 4.1 under Python 3.8-3.10Django 4.2 under Python 3.8-3.10Pull requests welcome!ExamplesModelsTimeStampedModelAbstractTimeStampedModelwith date created/modified fields:Use TimeStampedModel as base class for your modelfrombananas.modelsimportTimeStampedModelclassBook(TimeStampedModel):passthe timestamps can be accessed on the model as>>>book.date_created>>>book.date_modifiedUUIDModelAbstract model that uses a Django 1.8 UUID field as the primary key.frombananas.modelsimportUUIDModelclassUser(UUIDModel):display_name=models.CharField(max_length=255)email=models.EmailField()>>>user.idUUID('70cf1f46-2c79-4fc9-8cc8-523d67484182')>>>user.pkUUID('70cf1f46-2c79-4fc9-8cc8-523d67484182')SecretFieldCan be used to generate and store “safe” random bytes for authentication.frombananas.modelsimportSecretFieldclassUser(models.Model):# Ask for 32 bytes and require 24 bytes from urandomtoken=SecretField(num_bytes=32,min_bytes=24)>>>User.objects.create()# Token is generated automatically>>>user.token'3076f884da827809e80ced236e8da20fa36d0c27dd036bdd4afbac34807e5cf1'URLSecretFieldAn implementation of SecretField that generates an URL-safe base64 string
instead of a hex representation of the random bytes.frombananas.modelsimportURLSecretFieldclassUser(models.Model):# Generates an URL-safe base64 representation of the random valuetoken=URLSecretField(num_bytes=32,min_bytes=24)>>>user.token'WOgrNwqFKOF_LsHorJy_hGpPepjvVH7Uar-4Z_K6DzU-'ORMNewqueryset.dicts()with field renaming through kwargs, anddot-dictstyle results:frombananas.queryimportExtendedQuerySetclassBook(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'AdminCustom django admin stylesheet.WarningWork in progress. Only a few views styled completely as of now.# settings.pyINSTALLED_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.pyfrombananasimportadminurlpatterns=[# ...url(r"^admin/",include(admin.site.urls)),]# app/admin.py or somethingfromdjango.conf.urlsimporturlfrombananasimportadmin@admin.registerclassMyAdminView(admin.AdminView):defget_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.]defget(self,request):returnself.render("admin/template.html",{})defcustom_view(self,request):returnself.render("admin/custom.html",{})Admin APIDjango admin API for use withdjango-bananas.js(react admin site). This
feature requires installation with thedrfextra.# app/admin.py or somethingfrombananas.admin.api.mixinsimportBananasAPIfrombananas.admin.api.schemasimportschemafrombananas.admin.api.viewsimportBananasAdminAPIfrombananas.lazyimportlazy_titlefromdjango.utils.translationimportgettext_lazyas_fromrest_frameworkimportviewsetsclassCustomAdminAPI(BananasAdminAPI):name=lazy_title(_("custom"))@schema(query_serializer=SomeSerializer,responses={200:SomeSerializer})deflist(self,request):return...classSomeModelAdminAPI(BananasAPI,viewsets.ModelViewSet):serializer_class=SomeModelSerializerdeflist(self,request):return...# app/urls.py or somethingfrombananas.adminimportapifromdjango.conf.urlsimportinclude,pathfrom.adminimportCustomAdminAPI,SomeModelAdminAPIapi.register(CustomAdminAPI)api.register(SomeModelAdminAPI)urlpatterns=[path(r"^api/",include("bananas.admin.api.urls")),]# setting.pyADMIN={"API":{# Optional: override the default OpenAPI schemes"SCHEMES":["https"],}}Database URLsParse database information from a URL, kind of like SQLAlchemy.EnginesCurrently supported engines are:URI schemeEnginepgsql, postgres, postgresqldjango.db.backends.postgresql_psycopg2mysqldjango.db.backends.mysqloracledjango.db.backends.oraclesqlite, sqlite3django.db.backends.sqlite3mysqlgisdjango.contrib.gis.db.backends.mysqloraclegisdjango.contrib.gis.db.backends.oraclepostgisdjango.contrib.gis.db.backends.postgisspatialitedjango.contrib.gis.db.backends.spatialiteYou can add your own by runningregister(scheme, module_name)before parsing.database_conf_from_url(url)Return a django-style database configuration based onurl.param url:Database URLreturn:Django-style database configuration dictExample:>>>frombananas.urlimportdatabase_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 variablesbananas.environment.envis a wrapper aroundos.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 isNoneas you would expect.
What is more useful is the additional type-parsing.get_*methods it
provides:get_boolget_intget_list,get_set,get_tupleget_int:>>># env ONE=1>>>env.get_int("ONE")1>>>env.get_int("TWO")# Not setNone>>>env.get_int("TWO",-1)# Not set, default to -1-1get_bool:returnsTrueif the environment variable value is any of,
case-insensitive:"true""yes""on""1"returnsFalseif 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 validNone>>>env.get_bool("NO_HABLA",True)# Set, but not valid, with defaultTrue>>>env.get_bool("IS_NONE")# Not setNone>>>env.get_bool("IS_NONE",False)# Not set, default providedFalseget_tuple, get_list, get_set:Returns atuple,listorsetof 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 filesIs useful for getting the content of secrets stored in files. One usecase isdocker secrets.BANANAS_SECRETS_DIRcan be used to configure the directory that secrets live in. Defaults to/run/secrets/.>>>frombananasimportsecrets>>>secrets.get_secret("hemlis")"topsecret"bananas.drf.fencing - Fence DRF views with HTTP conditional headersBuilding blocks for composing HTTP conditionals to guard DRF views. Built to
work well in conjunction withBananasAdminAPIandTimeStampedModel. This
feature requires installation with thedrfextra.Fences add a header parameter to the exposed OpenAPI schema if you’re using
drf-yasg.allow_if_unmodified_sinceMake a view-set for aTimeStampedModelonly accept updates whenIf-Unmodified-Sincespecifies a date before thedate_modifiedof the
updated instance.Due to comparing datetime instances, usingallow_if_unmodified_sincerequires running Django with timezone support enabled,USE_TZ = TRUE.frombananas.drf.fencingimportFencedUpdateModelMixin,allow_if_unmodified_sinceclassItemAPI(FencedUpdateModelMixin,GenericViewSet):fence=allow_if_unmodified_since()serializer_class=ItemSerializerallow_if_matchMake a view-set that requires passing a version string inIf-Matchand
rejects requests when the given version does not match theversionattribute
of the updated instance.frombananas.drf.fencingimportFencedUpdateModelMixin,allow_if_matchclassItemAPI(FencedUpdateModelMixin,GenericViewSet):fence=allow_if_match(operator.attrgetter("version"))serializer_class=ItemSerializerFenceExample implementing a fence forIf-Modified-Since:importoperatorfromdrf_yasgimportopenapifromrest_frameworkimportstatusfromrest_framework.exceptionsimportAPIExceptionfrombananas.drf.fencingimportFence,header_date_parser,parse_date_modifiedclassNotModified(APIException):status_code=status.HTTP_304_NOT_MODIFIEDdefault_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"),)ContributingContributing 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.DevelopmentTesting and development requirements can be installed using package extrastestanddevrespectively. You’ll most likely always want to install thedrfextra when installingdev.To get started, setup a virtualenv and then install test requirements and run
tests and checks on Python 3.9/Django 3.1 with:python3-mpipinstall-e.[test]TOXENV=py39-django31,checkspython3-mtoxYou can install development requirements into your virtualenv. Linting and
formatting uses pre-commit which you could also install on a system level.python3-mpipinstall-e.[dev,drf]maketype-checkpre-commitrun--all-filesAfter installing pre-commit, you can enable hooks to have it run before you
publish pull requests.pre-commitinstall-tpre-pushAfter installingdevyou can also run tests without tox for rapid iteration
and select specific tests with thetestargument tomake test:maketesttest='tests.test_admin.APITest.test_logout'