diff options
| author | CoprDistGit <infra@openeuler.org> | 2023-05-15 09:05:08 +0000 |
|---|---|---|
| committer | CoprDistGit <infra@openeuler.org> | 2023-05-15 09:05:08 +0000 |
| commit | aa24f55767eb187aedeff94904900e896f91e413 (patch) | |
| tree | 05d2fff222a0791e83237c30cd55fa8d48a741ed /python-natural-keys.spec | |
| parent | 136753ba898cfe4d24144b55365ab33c4b9093e5 (diff) | |
automatic import of python-natural-keys
Diffstat (limited to 'python-natural-keys.spec')
| -rw-r--r-- | python-natural-keys.spec | 856 |
1 files changed, 856 insertions, 0 deletions
diff --git a/python-natural-keys.spec b/python-natural-keys.spec new file mode 100644 index 0000000..ee5cbcc --- /dev/null +++ b/python-natural-keys.spec @@ -0,0 +1,856 @@ +%global _empty_manifest_terminate_build 0 +Name: python-natural-keys +Version: 2.0.0 +Release: 1 +Summary: Enhanced support for natural keys in Django and Django REST Framework +License: MIT +URL: https://github.com/wq/django-natural-keys +Source0: https://mirrors.nju.edu.cn/pypi/web/packages/06/b5/d24c797c0eec5e301384eca6ef876aee2c341661877555c76448768e5949/natural-keys-2.0.0.tar.gz +BuildArch: noarch + +Requires: python3-html-json-forms + +%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. + +[](https://pypi.org/project/natural-keys/) +[](https://github.com/wq/django-natural-keys/releases) +[](https://github.com/wq/django-natural-keys/blob/main/LICENSE) +[](https://github.com/wq/django-natural-keys/stargazers) +[](https://github.com/wq/django-natural-keys/network) +[](https://github.com/wq/django-natural-keys/issues) + +[](https://github.com/wq/django-natural-keys/actions/workflows/test.yml) +[](https://pypi.org/project/natural-keys/) +[](https://pypi.org/project/natural-keys/) + + +## Usage + +*Django Natural Keys* is available via PyPI: + +```bash +# 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 one of the following: + + * A [`UniqueConstraint`][UniqueConstraint] in `Meta.constraints` (recommended), + * A tuple in [`Meta.unique_together`][unique_together], or + * A [model field][unique] (other than `AutoField`) with `unique=True` + +The first unique constraint found will be treated as the natural key for the model, and all of the necessary functions for working with natural keys will automatically work. + +```python +from natural_keys import NaturalKeyModel + +class Event(NaturalKeyModel): + name = models.CharField(max_length=255) + date = models.DateField() + class Meta: + constraints = [ + models.UniqueConstraint( + fields=('name', 'date'), + name='event_natural_key', + ) + ] + +class Note(models.Model): + event = models.ForeignKey(Event) + note = models.TextField() +``` +or +```python +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: + +```python +# 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 `ForeignKey`s to related models (which should also be `NaturalKeyModel` classes). This makes it possible to define complex, arbitrarily nested natural keys with minimal effort. + +```python +class Place(NaturalKeyModel): + name = models.CharField(max_length=255, unique=True) + +class Event(NaturalKeyModel): + place = models.ForeignKey(Place) + date = models.DateField() + class Meta: + constraints = [ + models.UniqueConstraint( + fields=('place', 'date'), + name='event_natural_key', + ) + ] +``` + +```python +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 `NaturalKeySerializer`s 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: +```python +# 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. + +```html +<form action="/events/" method="post"> + <input name="place[name]"> + <input type="date" name="date"> +</form> +``` +```js +// /events.json +[ + { + "id": 123, + "place": {"name": "ABC123"}, + "date": "2016-01-01" + } +] +``` +```html +<form action="/notes/" method="post"> + <input name="event[place][name]"> + <input type="date" name="event[date]"> + <textarea name="note"></textarea> +</form> +``` +```js +// /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. + +```python +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'),) +``` +```python +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]: + +```python +# 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. + +[natural keys]: https://docs.djangoproject.com/en/3.2/topics/serialization/#natural-keys +[UniqueConstraint]: https://docs.djangoproject.com/en/3.2/ref/models/constraints/#uniqueconstraint +[unique_together]: https://docs.djangoproject.com/en/3.2/ref/models/options/#unique-together +[unique]: https://docs.djangoproject.com/en/3.2/ref/models/fields/#unique + +[wq.db]: https://wq.io/wq.db/ +[Django REST Framework]: http://www.django-rest-framework.org/ +[vera.Report]:https://github.com/powered-by-wq/vera#report +[vera.Event]: https://github.com/powered-by-wq/vera#event +[ModelSerializer]: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer +[RelatedField]: https://www.django-rest-framework.org/api-guide/relations/ +[HTML JSON Forms]: https://github.com/wq/html-json-forms +[router]: https://wq.io/wq.db/router + + + + +%package -n python3-natural-keys +Summary: Enhanced support for natural keys in Django and Django REST Framework +Provides: python-natural-keys +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: python3-pip +%description -n python3-natural-keys +# 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. + +[](https://pypi.org/project/natural-keys/) +[](https://github.com/wq/django-natural-keys/releases) +[](https://github.com/wq/django-natural-keys/blob/main/LICENSE) +[](https://github.com/wq/django-natural-keys/stargazers) +[](https://github.com/wq/django-natural-keys/network) +[](https://github.com/wq/django-natural-keys/issues) + +[](https://github.com/wq/django-natural-keys/actions/workflows/test.yml) +[](https://pypi.org/project/natural-keys/) +[](https://pypi.org/project/natural-keys/) + + +## Usage + +*Django Natural Keys* is available via PyPI: + +```bash +# 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 one of the following: + + * A [`UniqueConstraint`][UniqueConstraint] in `Meta.constraints` (recommended), + * A tuple in [`Meta.unique_together`][unique_together], or + * A [model field][unique] (other than `AutoField`) with `unique=True` + +The first unique constraint found will be treated as the natural key for the model, and all of the necessary functions for working with natural keys will automatically work. + +```python +from natural_keys import NaturalKeyModel + +class Event(NaturalKeyModel): + name = models.CharField(max_length=255) + date = models.DateField() + class Meta: + constraints = [ + models.UniqueConstraint( + fields=('name', 'date'), + name='event_natural_key', + ) + ] + +class Note(models.Model): + event = models.ForeignKey(Event) + note = models.TextField() +``` +or +```python +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: + +```python +# 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 `ForeignKey`s to related models (which should also be `NaturalKeyModel` classes). This makes it possible to define complex, arbitrarily nested natural keys with minimal effort. + +```python +class Place(NaturalKeyModel): + name = models.CharField(max_length=255, unique=True) + +class Event(NaturalKeyModel): + place = models.ForeignKey(Place) + date = models.DateField() + class Meta: + constraints = [ + models.UniqueConstraint( + fields=('place', 'date'), + name='event_natural_key', + ) + ] +``` + +```python +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 `NaturalKeySerializer`s 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: +```python +# 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. + +```html +<form action="/events/" method="post"> + <input name="place[name]"> + <input type="date" name="date"> +</form> +``` +```js +// /events.json +[ + { + "id": 123, + "place": {"name": "ABC123"}, + "date": "2016-01-01" + } +] +``` +```html +<form action="/notes/" method="post"> + <input name="event[place][name]"> + <input type="date" name="event[date]"> + <textarea name="note"></textarea> +</form> +``` +```js +// /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. + +```python +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'),) +``` +```python +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]: + +```python +# 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. + +[natural keys]: https://docs.djangoproject.com/en/3.2/topics/serialization/#natural-keys +[UniqueConstraint]: https://docs.djangoproject.com/en/3.2/ref/models/constraints/#uniqueconstraint +[unique_together]: https://docs.djangoproject.com/en/3.2/ref/models/options/#unique-together +[unique]: https://docs.djangoproject.com/en/3.2/ref/models/fields/#unique + +[wq.db]: https://wq.io/wq.db/ +[Django REST Framework]: http://www.django-rest-framework.org/ +[vera.Report]:https://github.com/powered-by-wq/vera#report +[vera.Event]: https://github.com/powered-by-wq/vera#event +[ModelSerializer]: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer +[RelatedField]: https://www.django-rest-framework.org/api-guide/relations/ +[HTML JSON Forms]: https://github.com/wq/html-json-forms +[router]: https://wq.io/wq.db/router + + + + +%package help +Summary: Development documents and examples for natural-keys +Provides: python3-natural-keys-doc +%description help +# 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. + +[](https://pypi.org/project/natural-keys/) +[](https://github.com/wq/django-natural-keys/releases) +[](https://github.com/wq/django-natural-keys/blob/main/LICENSE) +[](https://github.com/wq/django-natural-keys/stargazers) +[](https://github.com/wq/django-natural-keys/network) +[](https://github.com/wq/django-natural-keys/issues) + +[](https://github.com/wq/django-natural-keys/actions/workflows/test.yml) +[](https://pypi.org/project/natural-keys/) +[](https://pypi.org/project/natural-keys/) + + +## Usage + +*Django Natural Keys* is available via PyPI: + +```bash +# 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 one of the following: + + * A [`UniqueConstraint`][UniqueConstraint] in `Meta.constraints` (recommended), + * A tuple in [`Meta.unique_together`][unique_together], or + * A [model field][unique] (other than `AutoField`) with `unique=True` + +The first unique constraint found will be treated as the natural key for the model, and all of the necessary functions for working with natural keys will automatically work. + +```python +from natural_keys import NaturalKeyModel + +class Event(NaturalKeyModel): + name = models.CharField(max_length=255) + date = models.DateField() + class Meta: + constraints = [ + models.UniqueConstraint( + fields=('name', 'date'), + name='event_natural_key', + ) + ] + +class Note(models.Model): + event = models.ForeignKey(Event) + note = models.TextField() +``` +or +```python +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: + +```python +# 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 `ForeignKey`s to related models (which should also be `NaturalKeyModel` classes). This makes it possible to define complex, arbitrarily nested natural keys with minimal effort. + +```python +class Place(NaturalKeyModel): + name = models.CharField(max_length=255, unique=True) + +class Event(NaturalKeyModel): + place = models.ForeignKey(Place) + date = models.DateField() + class Meta: + constraints = [ + models.UniqueConstraint( + fields=('place', 'date'), + name='event_natural_key', + ) + ] +``` + +```python +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 `NaturalKeySerializer`s 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: +```python +# 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. + +```html +<form action="/events/" method="post"> + <input name="place[name]"> + <input type="date" name="date"> +</form> +``` +```js +// /events.json +[ + { + "id": 123, + "place": {"name": "ABC123"}, + "date": "2016-01-01" + } +] +``` +```html +<form action="/notes/" method="post"> + <input name="event[place][name]"> + <input type="date" name="event[date]"> + <textarea name="note"></textarea> +</form> +``` +```js +// /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. + +```python +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'),) +``` +```python +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]: + +```python +# 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. + +[natural keys]: https://docs.djangoproject.com/en/3.2/topics/serialization/#natural-keys +[UniqueConstraint]: https://docs.djangoproject.com/en/3.2/ref/models/constraints/#uniqueconstraint +[unique_together]: https://docs.djangoproject.com/en/3.2/ref/models/options/#unique-together +[unique]: https://docs.djangoproject.com/en/3.2/ref/models/fields/#unique + +[wq.db]: https://wq.io/wq.db/ +[Django REST Framework]: http://www.django-rest-framework.org/ +[vera.Report]:https://github.com/powered-by-wq/vera#report +[vera.Event]: https://github.com/powered-by-wq/vera#event +[ModelSerializer]: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer +[RelatedField]: https://www.django-rest-framework.org/api-guide/relations/ +[HTML JSON Forms]: https://github.com/wq/html-json-forms +[router]: https://wq.io/wq.db/router + + + + +%prep +%autosetup -n natural-keys-2.0.0 + +%build +%py3_build + +%install +%py3_install +install -d -m755 %{buildroot}/%{_pkgdocdir} +if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi +if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi +if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi +if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi +pushd %{buildroot} +if [ -d usr/lib ]; then + find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/lib64 ]; then + find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/bin ]; then + find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/sbin ]; then + find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst +fi +touch doclist.lst +if [ -d usr/share/man ]; then + find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst +fi +popd +mv %{buildroot}/filelist.lst . +mv %{buildroot}/doclist.lst . + +%files -n python3-natural-keys -f filelist.lst +%dir %{python3_sitelib}/* + +%files help -f doclist.lst +%{_docdir}/* + +%changelog +* Mon May 15 2023 Python_Bot <Python_Bot@openeuler.org> - 2.0.0-1 +- Package Spec generated |
