Skip to main content

Enhanced support for natural keys in Django and Django REST Framework

Project description

Django Natural Keys

Enhanced support for natural keys in Django and Django REST Framework. Extracted from wq.db for general use.

Django Natural Keys provides a number of useful model methods (e.g. get_or_create_by_natural_key()) that speed up working with natural keys in Django. The module also provides a couple of serializer classes that streamline creating REST API support for models with natural keys.

Latest PyPI Release Release Notes License GitHub Stars GitHub Forks GitHub Issues

Travis Build Status Python Support Django Support

Usage

Django Natural Keys is available via PyPI:

# Recommended: create virtual environment
# python3 -m venv venv
# . venv/bin/activate
pip install natural-keys

Model API

To use natural keys in vanilla Django, you need to define a natural_key() method on your Model class and a get_natural_key() method on the Manager class. With Django Natural Keys, you can instead extend NaturalKeyModel and define a unique_together property on your Model's Meta class or use a field with unique=True. The first unique_together entry or the first unique field (except an AutoField) will be treated as the natural key for the model, and all of the necessary functions for working with natural keys will automatically work.

from natural_keys import NaturalKeyModel

class Event(NaturalKeyModel):
    name = models.CharField(max_length=255)
    date = models.DateField()
    class Meta:
        unique_together = (('name','date'),)

class Note(models.Model):
    event = models.ForeignKey(Event)
    note = models.TextField()

or

from natural_keys import NaturalKeyModel

class Event(NaturalKeyModel):
    name = models.CharField(unique=True)

The following methods will then be available on your Model and its Manager:

# Default Django methods
instance = Event.objects.get_by_natural_key('ABC123', date(2016, 1, 1))
instance.natural_key == ('ABC123', date(2016, 1, 1))

# get_or_create + natural keys
instance, is_new = Event.objects.get_or_create_by_natural_key('ABC123', date(2016, 1, 1))

# Like get_or_create_by_natural_key, but discards is_new
# Useful for quick lookup/creation when you don't care whether the object exists already
instance = Event.objects.find('ABC123', date(2016, 1, 1))
note = Note.objects.create(
     event=Event.objects.find('ABC123', date(2016, 1, 1)),
     note="This is a note"
)
instance == note.event

# Inspect natural key fields on a model without instantiating it
Event.get_natural_key_fields() == ('name', 'date')

Nested Natural Keys

One key feature of Django Natural Keys is that it will automatically traverse ForeignKeys to related models (which should also be NaturalKeyModel classes). This makes it possible to define complex, arbitrarily nested natural keys with minimal effort.

class Place(NaturalKeyModel):
    name = models.CharField(max_length=255, unique=True)

class Event(NaturalKeyModel):
    place = models.ForeignKey(Place)
    date = models.DateField()
    class Meta:
        unique_together = (('place', 'date'),)
Event.get_natural_key_fields() == ('place__name', 'date')
instance = Event.find('ABC123', date(2016, 1, 1))
instance.place.name == 'ABC123'

REST Framework Support

Django Natural Keys provides several integrations with Django REST Framework, primarily through custom Serializer classes. In most cases, you will want to use either:

  • NaturalKeyModelSerializer, or
  • The natural_key_slug pseudo-field (see below)

If you have only a single model with a single char field for its natural key, you probably do not need to use either of these integrations. In your view, you can just use Django REST Framework's built in lookup_field to point directly to your natural key.

NaturalKeyModelSerializer

NaturalKeyModelSerializer facilitates handling complex natural keys in your rest API. It can be used with a NaturalKeyModel, or (more commonly) a model that has a foreign key to a NaturalKeyModel but is not a NaturalKeyModel itself. (One concrete example of this is the vera.Report model, which has a ForeignKey to vera.Event, which is a NaturalKeyModel).

NaturalKeyModelSerializer extends DRF's ModelSerializer, but uses NaturalKeySerializer for each foreign key that points to a NaturalKeyModel. When update() or create()ing the primary model, the nested NaturalKeySerializers will automatically create instances of referenced models if they do not exist already (via the find() method described above). Note that NaturalKeyModelSerializer does not override DRF's default behavior for other fields, whether or not they form part of the primary model's natural key.

NaturalKeySerializer can technically be used as a top level serializer, though this is not recommended. NaturalKeySerializer is designed for dealing with nested natural keys and does not support updates or non-natural key fields. Even when used together with NaturalKeyModelSerializer, NaturalKeySerializer never updates an existing related model instance. Instead, it will repoint the foreign key to another (potentially new) instance of the related model. It may help to think of NaturalKeySerializer as a special RelatedField class rather than as a Serializer per se.

You can use NaturalKeyModelSerializer with Django REST Framework and/or wq.db just like any other serializer:

# Django REST Framework usage example
from rest_framework import viewsets
from rest_framework import routers
from natural_keys import NaturalKeyModelSerializer
from .models import Event, Note

class EventSerializer(NaturalKeyModelSerializer):
    class Meta:
        model = Event

class NoteSerializer(NaturalKeyModelSerializer):
    class Meta:
        model = Note

class EventViewSet(viewsets.ModelViewSet):
    queryset = Event.objects.all()
    serializer_class = EventSerializer

class NoteViewSet(viewsets.ModelViewSet):
    queryset = Note.objects.all()
    serializer_class = NoteSerializer

router = routers.DefaultRouter()
router.register(r'events', EventViewSet)
router.register(r'notes', NoteViewSet)

# wq.db usage example
from wq.db import rest
from natural_keys import NaturalKeyModelSerializer
from .models import Event, Note

rest.router.register_model(Note, serializer=NaturalKeyModelSerializer)
rest.router.register_model(Event, serializer=NaturalKeyModelSerializer)

Once this is set up, you can use your REST API to create and view your NaturalKeyModel instances and related data. To facilitate integration with regular HTML Forms, Django Natural Keys is integrated with the HTML JSON Forms package, which supports nested keys via an array naming convention, as the examples below demonstrate.

<form action="/events/" method="post">
  <input name="place[name]">
  <input type="date" name="date">
</form>
// /events.json
[
    {
        "id": 123,
        "place": {"name": "ABC123"},
        "date": "2016-01-01"
    }
]
<form action="/notes/" method="post">
  <input name="event[place][name]">
  <input type="date" name="event[date]">
  <textarea name="note"></textarea>
</form>
// /notes.json
[
    {
        "id": 12345,
        "event": {
            "place": {"name": "ABC123"},
            "date": "2016-01-01"
        },
        "note": "This is a note"
    }
]

Natural Key Slugs

As an alternative to using NaturalKeyModelSerializer / NaturalKeySerializer, you can also use a single slug-like field for lookup and serialization. NaturalKeyModel (and its associated queryset) defines a pseudo-field, natural_key_slug, for this purpose.

class Place(NaturalKeyModel):
    name = models.CharField(max_length=255, unique=True)

class Room(NaturalKeyModel)
    place = models.ForeignKey(Place, models.ON_DELETE)
    name = models.CharField(max_length=255)

    class Meta:
        unique_together = (('place', 'name'),)
room = Room.objects.find("ABC123", "MainHall")
assert(room.natural_key_slug == "ABC123-MainHall")
assert(room == Room.objects.get(natural_key_slug="ABC123-MainHall"))

You can expose this functionality in your REST API to expose natural keys instead of database-generated ids. To do this, you will likely want to do the following:

  1. Create a regular serializer with id = serializers.ReadOnlyField(source='natural_key_slug')
  2. Set lookup_field = 'natural_key_slug' on your ModelViewSet (or similar generic class) and update the URL registration accordingly
  3. Ensure foreign keys on any related models are serialized with serializers.SlugRelatedField(slug_field='natural_key_slug')

In wq.db, all three of the above can be achieved by setting the "lookup" attribute when registering with the router:

# myapp/rest.py
from wq.db import rest
from .models import Room

rest.router.register_model(
    Room,
    fields='__all__',
    lookup='natural_key_slug',
)

Note that the natural_key_slug may not behave as expected if any of the component values contain the delimiter character (- by default). To mitigate this, you can set natural_key_separator on the model class to another character.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

natural-keys-1.5.1.tar.gz (17.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

natural_keys-1.5.1-py3-none-any.whl (10.3 kB view details)

Uploaded Python 3

File details

Details for the file natural-keys-1.5.1.tar.gz.

File metadata

  • Download URL: natural-keys-1.5.1.tar.gz
  • Upload date:
  • Size: 17.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.5.2

File hashes

Hashes for natural-keys-1.5.1.tar.gz
Algorithm Hash digest
SHA256 b0bc3f17baf557fa2aed3bb94728adcfaef3dc068f5eb14d2d00c76169ed6fd3
MD5 72c217efc19887ae4c2bab1522c197ab
BLAKE2b-256 cdbda2db13159e4e3613a46eba1a59e98fbf8a4a699efab4aadda433c9e43635

See more details on using hashes here.

File details

Details for the file natural_keys-1.5.1-py3-none-any.whl.

File metadata

  • Download URL: natural_keys-1.5.1-py3-none-any.whl
  • Upload date:
  • Size: 10.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.5.2

File hashes

Hashes for natural_keys-1.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ea4a19e3ce67a52e2f2745443113913bab748449ab345467064819e25c26d156
MD5 b894a63a6eaeda660ee534916e315bde
BLAKE2b-256 af8cde54ee11fcc068386426aba93a0879545586878b8dac4f837f4a3eec85b2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page