django-hierarkey¶
This package allows you to attach a key-value store to a model, e.g. to store preferences of a user or a customer. The package supports arbitrary datatypes, defaults and model hierarchies, i.e. you can define a different model instance as your instance’s parent and the values of the parent instance will be used as default values for the child instances.
Documentation content¶
Concepts¶
Hierarkey is built to store key-value pairs. For example, you could use it to store arbitrary preferences of your users that you do not want to create model fields for because you have too many of them or because you generate them dynamically.
Storage hierarchy¶
Hierarkey is built as a hierarchical store. For example, in your application users might be associated with an organization. In that case, you can store key-value pairs with the user, the organization or globally for your application.
If you fetch a value for a user, the value set for the user will be returned. If
the value has never been set for the user, the value set for the organization will
be returned. If the value has never been set for the organization, the globally
configured value will be returned. If no value has ever been set, the hardcoded
default will be returned. If no default exists for the key, None
will be returned.
Note that this is only an example, you could build this hierarchy in any way you want.
Storage and data types¶
The key-value pairs are stored into one database model per level of your hierarchy.
The values will be stored in a TextField
, so they will need to be serialized first.
When querying values, they need to be deserialized to get the original data type.
If you have hardcoded a default value for a key, it will have a type
associated. In this case, if you don’t need to pass a type when querying, the
hardcoded type will be used for deserialization.
Otherwise, as hierarkey is not able to detect the data type from the saved
data, you need to pass the desired data type to the query function as
as_type
(see the API reference).
Currently, the following data types are supported out of the box:
str
,bool
,int
,float
list
anddict
with members that are serializableDecimal
datetime
,time
,date
- Any Django model instances (only the primary key is stored, so this behaves like an unconstrained foreign key)
django.core.files.File
Hierarkey is built in a way that allows you to easily add custom defaults and your own data types.
Getting started¶
Install hierarkey¶
You only need to install the hierarkey package, for example using pip:
$ pip install django-hierarkey
That’s it!
Attach a storage to a single models¶
As a first example, we will attach a storage object to a single model. For example, you could use this to store settings for every user. If you want to use the hierarchical features of hierarkey, just skip to the next example.
First of all, in your models.py
, we will create a Hierarkey
object:
from hierarkey.models import Hierarkey
hierarkey = Hierarkey(attribute_name='settings')
The attribute name defines the name that you will use later to access the storage. This allows you to use multiple separated storage hierarchies within one model, if you want to. Next, add a decorator to the model you want to associate the storage with:
@hierarkey.add()
class User(models.Model):
....
Note
The parentheses in the call to the @hierarkey.add()
decorator are required.
Refer to the API documentation for more information on its parameters.s
Now, you need to create a database migration and apply it:
$ python manage.py makemigrations
$ python manage.py migrate
Build a hierarchical storage¶
To build a hierarchy, we need multiple models. In our example, we will have a three-level hierarchy, consisting of global settings, organization settings and user settings. You can use more levels if you want to, or you can omit the global settings. There can only be one level of global settings.
As in the previous example, we first create a Hierarkey
object:
from hierarkey.models import GlobalSettingsBase, Hierarkey
hierarkey = Hierarkey(attribute_name='settings')
Next, we define a class, and attach the global settings. This class can be empty and is not needed except for consistency in the hierarkey API. You can define it like this:
@hierarkey.set_global()
class GlobalSettings(GlobalSettingsBase):
pass
Then, we add our other two layers. The organization layer works the same way as in the first example above. In contrast, for the user model we have to specify the name of the parent relation:
@hierarkey.add()
class Organization(models.Model):
...
@hierarkey.add(parent_field='organization')
class User(models.Model):
organization = models.ForeignKey(Organization)
...
Now, you need to create a database migration and apply it:
$ python manage.py makemigrations
$ python manage.py migrate
Set default values¶
You can add default values by calling a method on the Hierarkey
object specifying the key, the value
and the type:
hierarkey.add_default('key', 'value', bool)
Access the settings storage¶
On an instance of your model, you can access the settings storage under the name you specified as the
attribute_name
further above.
You can read and write the values in the key-value store in three ways. First, by attribute access:
print(user.settings.theme)
user.settings.theme = 'dark'
del user.settings.theme
Note
Attribute access is unsupported for key starting with an underscore.
Second, by item access:
print(user.settings['theme'])
user.settings['theme'] = 'dark'
del user.settings['theme']
And third, using explicit methods:
print(user.settings.get('theme'))
user.settings.set('theme', 'dark')
user.settings.delete('theme')
All changes are written to the database instantly, while values are read eagerly and are being cached.
Deserialization will only be automatically performed for keys that have a default value specified in code. If you want to deserialize other keys, you need to use the explicit getter methods and specify the type yourself:
user.settings.get('theme', as_type=int)
To access the global settings, you can instantiate the global settings class you defined before:
GlobalSettings().settings.get(…)
Next steps¶
You can now continue reading either about Forms or in the API Reference.
API Reference¶
Everything not listed on this page is considered a private API and should not be called from outside.
Model definition¶
-
class
hierarkey.models.
Hierarkey
(attribute_name)¶ The Hierarkey object represents one complete key-value store hierarchy. It can have one global and multiple object-level storages attached and holds default values as well as custom type serialization info.
Parameters: attribute_name – The name for the attribute on the model instances that will allow access to the storage, e.g. settings
.-
add
(cache_namespace: str = None, parent_field: str = None) → type¶ Decorator. Attaches a global key-value store to a Django model.
Parameters: - cache_namespace – Optional. A custom namespace used for caching. By default this is
constructed from the name of the class this is applied to and
the
attribute_name
of thisHierarkey
object. - parent_field – Optional. The name of a field of this model that refers to the parent
in the hierarchy. This must be a
ForeignKey
field.
- cache_namespace – Optional. A custom namespace used for caching. By default this is
constructed from the name of the class this is applied to and
the
-
add_default
(key: str, value: Optional[str], default_type: type = <class 'str'>) → None¶ Adds a default value and a default type for a key.
Parameters: - key – Key
- value – Serialized default value, i.e. a string or
None
. - default_type – The type to deserialize values for this key to, defaults to
str
.
-
add_type
(type: type, serialize: Callable[[Any], str], unserialize: Callable[[str], Any]) → None¶ Adds serialization support for a new type.
Parameters: - type – The type to add support for.
- serialize – A callable that takes an object of type
type
and returns a string. - unserialize – A callable that takes a string and returns an object of type
type
.
-
set_global
(cache_namespace: str = None) → type¶ Decorator. Attaches the global key-value store of this hierarchy to an object.
Parameters: cache_namespace – Optional. A custom namespace used for caching. By default this is constructed from the name of the class this is applied to and the attribute_name
of thisHierarkey
object.
-
-
class
hierarkey.models.
GlobalSettingsBase
¶ Base class for objects with a global settings storage attached. This class does not add any functionality, it only makes global settings behave more consistent to object-level settings.
Storage access¶
-
class
hierarkey.proxy.
HierarkeyProxy
¶ If you add a hierarkey storage to a model, the model will get a new attribute (e.g.
settings
) containing a key-value store that is managed by this class.This class allows access to settings via attribute access, item access or using the documented methods. You should not instantiate this class yourself.
-
delete
(key: str) → None¶ Deletes a setting from this object’s storage.
The write to the database is performed immediately and the cache in the cache backend is flushed. The cache within this object will be updated correctly.
-
flush
() → None¶ Discards both the state within this object as well as the cache in Django’s cache backend.
-
freeze
() → dict¶ Returns a dictionary of all settings set for this object, including any values of its parents or hardcoded defaults.
-
get
(key: str, default=None, as_type: type = None, binary_file: bool = False)¶ Get a setting specified by
key
. Normally, settings are strings, but if you put non-strings into the settings object, you can request deserialization by specifyingas_type
. If the key does not have a harcdoded default type, omittingas_type
always will get you a string.If the setting with the specified name does not exist on this object, any parent object up to the global settings layer (if configured) will be queried. If still no value is found, a default value set in ths source code will be returned if one exists. If not, the value of the
default
argument of this method will be returned instead.If you receive a
File
object, it will already be opened. You can specify thebinary_file
flag to indicate that it should be opened in binary mode.
-
set
(key: str, value: Any) → None¶ Stores a setting in the database and connects it to its object.
The write to the database is performed immediately and the cache in the cache backend is flushed. The cache within this object will be updated correctly.
-
Forms¶
-
class
hierarkey.forms.
HierarkeyForm
(*args, obj, attribute_name, **kwargs)¶ This is a custom subclass of
django.forms.Form
that you can use to set values for any keys. See the Forms chapter of the documentation for more details.-
get_new_filename
(name: str) → str¶ Returns the file name to use based on the original filename of an uploaded file. By default, the file name is constructed as:
<model_name>-<attribute_name>/<primary_key>/<original_basename>.<random_nonce>.<extension>
-
save
() → None¶ Saves all changed values to the database.
-
Forms¶
Hierarkey provides a form base class to manipulate values of a key-value store. You just define your form as you normally would:
from django import forms
from hierarkey.forms import HierarkeyForm
class MySettingsForm(HierarkeyForm):
theme = forms.ChoiceField(
choices=(('light', 'light'), ('dark', 'dark')
)
...
You can use any form field that results in a data type that can be serialized and deserialized by hierarkey. This includes most form fields defined by Django and even custom types.
To save the data, you can call the save()
method on the form.
Note that the form takes two additional arguments: The object that you want to update the values for
(i.e. a GlobalSettings
object or a model instance) and the attribute_name
the storage is located at.
When using a class-based FormView
, you could integrate it like this:
from django.views.generic import FormView
class MySettingsView(FormView):
form_class = MySettingsForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['attribute_name'] = 'settings'
kwargs['obj'] = User.objects.get(...)
return kwargs
def form_valid(self, form):
form.save()
return super().form_valid(form)
Note
Initial values for the form will be taken from the settings storage, i.e. from the given object and the global and hardcoded defaults. Initial values set directly on the form or field layer are not supported.
Custom types¶
You can add support for serializing and deserializing custom types like this:
class MyMessageType:
def __init__(self, msg):
self.msg = msg
hierarkey.add_type(
MyMessageType,
lambda v: v.foo,
lambda v: MyMessageType(v)
)
...
# Serialize
user.settings.set('myproperty', MyMessageType('Hello'))
# Deserizalize
# will return MyMessageType('Hello')
user.settings.get('myproperty', as_type=MyMessageType)
File handling¶
Hierarkey has rudimentary support for saving a file
into the key-value storage. In this case, not the content
of the file will be saved in the key-value store. Instead, only the name of the file within the configured Django
storage backend will be saved.
You need to save the file to the storage backend yourself and then pass the File
object to hierarkey.
When you access the key in the store, the file://
prefix will automatically be detected and hierarkey will use
your default storage backend to open the file for you. The binary_file
flag of the get()
method allows you to
open the file in binary mode.
When you use our forms support, this is done automatically for you. You can just specify a
normal django.forms.FileField
field on the model and HierarkeyForm
will deal with storing the file
to the default storage backend as well as deleting and replacing files. The filename will be automatically generated
based on the primary key of your model, the key in the storage, and a random nonce. You can change this behaviour by
overriding get_new_filename()
on your form.
Author and License¶
This package has been created by Raphael Michel and is published under the terms of the Apache License 2.0.