Git Product home page Git Product logo

django-preserialize's Introduction

django-preserialize

Build Status Coverage Status Bitdeli Badge

django-preserialize is a one-stop shop for ensuring an object is free of Model and QuerySet instances. By default, all non-relational fields will be included as well as the primary keys of local related fields. The resulting containers will simply be dicts and lists.

Install

pip install django-preserialize

Docs

A serialized user object might look like this:

>>> from preserialize.serialize import serialize
>>> serialize(user)
{
    'date_joined': datetime.datetime(2009, 5, 16, 15, 52, 40),
    'email': u'[email protected]',
    'first_name': u'Jon',
    'groups': [5],
    'id': 1,
    'is_active': True,
    'is_staff': True,
    'is_superuser': True,
    'last_login': datetime.datetime(2012, 3, 3, 17, 40, 41, 927637),
    'last_name': u'Doe',
    'password': u'!',
    'user_permissions': [1, 2, 3],
    'username': u'jdoe'
}

This can then be passed off to a serializer/encoder, e.g. JSON, to turn it into a string for the response body (or whatever else you want to do).

Serialize Options

Some fields may not be appropriate or relevent to include as output. To customize which fields get included or excluded, the following arguments can be passed to serialize:

fields

A list of fields names to include. Method names can also be specified that will be called when being serialized. Default is all local fields and local related fields. See also: exclude, aliases

exclude

A list of fields names to exclude (this takes precedence over fields). Default is None. See also: fields, aliases

related

A dict of related object accessors and configs (see below) for handling related objects.

values_list

This option only applies to QuerySets. Returns a list of lists with the field values (like Django's ValuesListQuerySet). Default is False.

flat

Applies only if one field is specified in fields. If applied to a QuerySet and if values_list is True the values will be flattened out. If applied to a model instance, the single field value will used (in place of the dict). Note, if merge is true, this option has not effect. Default is True.

prefix

A string to be use to prefix the dict keys. To enable dynamic prefixes, the prefix may contain '%(accessor)s' which will be the class name for top-level objects or the accessor name for related objects. Default is None.

aliases

A dictionary that maps the keys of the output dictionary to the actual field/method names referencing the data. Default is None. See also: fields

camelcase

Converts all keys to a camel-case equivalent. This is merely a convenience for conforming to language convention for consumers of this content, namely JavaScript. Default is False.

allow_missing

Allow for missing fields (rather than throwing an error) and fill in the value with None.

Hooks

Hooks enable altering the objects that are serialized at each level.

prehook

A function that takes and returns an object. For QuerySets it can be used for filtering or annotating additional data to each model instance. For Model instances it can be prefetching additional data, swapping out an instance or whatever is necessary prior to serialization.

Since filtering QuerySets is a common use case, a simple dict can be supplied instead of a function that will be passed to the filter method.

Here are two examples for filtering posts by the requesting user.

The shorthand method of using a dict:

def view(request):
    template = {
        'related': {
            'posts': {
                'prehook': {'user': request.user},
            }
        }
    }
    ...

For applying conditional logic, a function can be used:

from functools import partial

def filter_by_user(queryset, request):
    if not request.user.is_superuser:
        queryset = queryset.filter(user=request.user)
    return queryset

def view(request):
    template = {
        'related': {
            'posts': {
                'prehook': partial(filter_by_user, request=request)
            }
        }
    }
    ...

posthook

A function that takes the original model instance and the serialized attrs for post-processing. This is specifically useful for augmenting or modifying the data prior to being added to the large serialized data structure.

Even if the related object (like posts above) is a QuerySet, this hook is applied per object in the QuerySet. This is because it would rarely ever be necessary to process a list of objects as a whole since filtering can already be performed above (using the prehook) prior to serialization.

Here is an example of adding resource links to the output data based on the serialized attributes:

from functools import partial
from django.core.urlresolvers import reverse

def add_resource_links(instance, attrs, request):
    uri = request.build_absolute_uri
    attrs['_links'] = {
        'self': {
            'href': uri(reverse('api:foo:bar', kwargs={'pk': attrs.id})),
        },
        ...
    }
    return attrs

template = {
    'posthook': partial(add_resource_links, request=request),
    ...
}

Examples

# The field names listed are after the mapping occurs
>>> serialize(user, fields=['username', 'full_name'], aliases={'full_name': 'get_full_name'}, camelcase=True)
{
    'fullName': u'Jon Doe',
    'username': u'jdoe'
}

>>> serialize(user, exclude=['password', 'groups', 'permissions'])
{
    'date_joined': datetime.datetime(2009, 5, 16, 15, 52, 40),
    'email': u'[email protected]',
    'first_name': u'Jon',
    'id': 1,
    'is_active': True,
    'is_staff': True,
    'is_superuser': True,
    'last_login': datetime.datetime(2012, 3, 3, 17, 40, 41, 927637),
    'last_name': u'Doe',
    'username': u'jdoe'
}

>>> serialize(user, fields=['foo', 'bar', 'baz'], allow_missing=True)
{
    'foo': None,
    'bar': None,
    'baz': None,
}

Related Objects

Composite resources are common when dealing with data that have tight relationships. A user and their user profile is an example of this. It is inefficient for a client to have to make two separate requests for data that is typically always consumed together.

serialize supports the related keyword argument for defining options for relational fields. The following additional argument (to the above) may be defined:

merge

This option only applies to local ForeignKey or OneToOneField. This allows for merging the related object's fields into the parent object.

>>> serialize(user, related={'groups': {'fields': ['name']}, 'profile': {'merge': True}})
{
    'username': u'jdoe',
    'groups': [{
        'name': u'Managers'
    }]
    # profile attributes merged into the user
    'twitter': '@jdoe',
    'mobile': '123-456-7890',
    ...
}

Conventions

Define a template dict for each model that will be serialized.

Defining a template enables reuse across different serialized objects as well as increases readability and maintainability. Deconstructing the above example, we have:

# Render a list of group names.. note that 'values_list'
# options is implied here since there is only one field
# specified. It is here to be explicit.
group_template = {
    'fields': ['name'],
    'values_list': True,
}

# User profiles are typically always wanted to be merged into
# User, so we can add the 'merge' option. Remember this simply
# gets ignored if 'UserProfile' is the top-level object being
# serialized.
profile_template = {
    `exclude`: ['user'],
    'merge': True,
}

# Users typically always include some related data (groups and their
# profile), so we can reference the above templates in this one.
user_template = {
    'exclude': ['password', 'user_permissions'],
    'related': {
        'groups': group_template,
        'profile': profile_template,
    }
}

# Everyone is the pool!
users = User.objects.all()
# Now we can use Python's wonderful argument _unpacking_ syntax.
# Clean.
serialize(users, **user_template)

FAQ

Does the serializer only understand model fields?

No. In fact it is smart about accessing the field. The following steps are taken when attempting to get the value:

  1. Use hasattr to check if an attribute/property is present
  2. If the object support __getitem__, check if the key is present

Assuming one of those two methods succeed, it will check if the value is callable and will call it (useful for methods). If the value is a RelatedManager, it will resolve the QuerySet and recursive downstream.

Does the serializer only support model instances?

No. It is not always the case that a single model instance or queryset is the source of data for a resource. serialize also understands dicts and any iterable of dicts. They will be treated similarly to the model instances.

My model has a ton of fields and I don't want to type them all out. What do I do?

The fields and exclude options understands four pseudo-selectors which can be used in place of typing out all of a model's field names (although being explicit is typically better).

:pk

The primary key field of the model

:local

All local fields on a model (including local foreign keys and many-to-many fields)

:related

All related fields (reverse foreign key and many-to-many)

:all

A composite of all selectors above and thus any an all fields on or related to a model

You can use them like this:

# The two selectors here are actually the default, but defined
# for the example.
serialize(user, fields=[':pk', ':local', 'foo'], exclude=['password'])

CHANGELOG

2013-05-01

  • Update posthook to take the original instance as the first argument and the serialized data as the second argument
  • Ensure the passed object is returned from serialize even if it does not qualify to be processed

2013-04-29

  • Fix bug where the flat option was not respecting merge
    • If merge is set, it takes precedence over flat

2013-04-28

  • Add prehook and posthook options
  • Add support for flat option for model instances with a single field
  • Rename key_map to aliases for clarity
  • Rename key_prefix to prefix
    • It is implied the prefix applies to the keys since this a serialization utility
  • Internal clean up
  • Correct documentation regarding the flat option
    • It was incorrectly named flatten

django-preserialize's People

Contributors

bruth avatar romantolkachyov avatar stargazer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

django-preserialize's Issues

Please support Django 1.11.x and 2.0.x

I'm a Nitrate maintainer which uses this cool package to serialize data. It has been migrated to work with Django 1.11.x and 2.0.x will be the next version to work with. Could you please update to support these two Django versions? Thank you very much.

Evaluating querysets

Hi,

Firstly, thanks for this wonderful piece of software :)
I have a short question. Method queryset_to_list from the serialize.py module,

def queryset_to_list(queryset, **options):
    ...
    return [model_to_dict(x, **options) for x in queryset.iterator()]

evaluates the queryset using an iterator. Doing so, has the downside that a queryset that has been evaluated already (on application level, for example), will be re-evaluated here. My main problem with this, is that any ad-hoc data attached to the model instances that were previously loaded when the queryset was first evaluated, are lost.

Contrary to that, if the queryset is evaluated normally without an iterator,

def queryset_to_list(queryset, **options):
    ...
    return [model_to_dict(x, **options) for x in queryset]

it won't be re-evaluated again, if it has already been evaluated on application level, and therefore the implication I mentioned above is eliminated.

I'm curious on your thoughts,

Serializer class improvement suggestion

Hi,

first of all, thanks for the recent update. Serializer class seems useful to get the code organized better. I have some additional suggestions though that i hope make sense.

The Serializer-level options could be perhaps implemented as class/instance attributes. This would make it possible to have a base serializer class from which defaults could be inherited. What could also be useful is for the related dictionary to accept Serializer class / instances as values. With these two things together it will be possible to declare and assign common settings in just one place - the base serializer class. It makes sense in case of hooks, for example, when a common logic needs to be executed for any instance of every serializable model.

Add ability to define object hooks for customizing output during serialization

Often times there are slight representation tweaks that are desirable or the ability to augment a value to the resource. For model objects this can typically be done using model properties or methods. Some things (like building the resource URL) is not appropriate to add to the model in my opinion. Other must be simple value/type formatting.

A new option called hooks could be defined which is a dict where the keys are the output fields and the values are functions or a static value (good for placeholders or filling in _gaps):

>>> template = {
...         'fields': [':pk', 'first_name', 'last_name', 'permissions'],
...         'hooks': {
...             'permissions': lambda x: str(x),
...         }
...     },
... }

>>> serialize(user, **template)
{
    'id': 1,
    'first_name': 'John',
    'last_name': 'Doe',
    'permissions': [
         'auth | group | Can add group',
         'auth | group | Can change group',
         'auth | group | Can delete group',
         'auth | permission | Can add permission',
         'auth | permission | Can change permission',
         # ...
    ],
}

AttributeError: 'Options' object has no attribute 'get_all_related_objects'

Hello,

I get this error:

AttributeError: 'Options' object has no attribute 'get_all_related_objects'

This is my code:

  user  = User.objects.get(pk=user_id)
   print(serialize(user))
class User(models.Model):
    displayName = models.CharField(max_length=28)
    email = models.CharField(max_length=28)
    isNearby = models.BooleanField(default=False)

Any Idea?

Issue with ThumbnailerImageField (ImageField?)

Hi,

I'm using Easy Thumbnails for managing resized versions of images. In my model i have a ThumbnailerImageField which stores a URL to the original image. When trying to serialize an instance of this model, i get the JSON exception:

<ThumbnailerImageFieldFile: ...> is not JSON serializable

ThumbnailerImageField extends Django's original ImageField and the Django-provided serialization mechanism works well in this case so i guess it may be a bug.

Implement resource documentation generator for models

The metadata that can be obtained from options passed into serialize can be used to extract various field labels, docstrings, etc. about the resource. A new function serialize_docs (a clever name) could take the same options and a top-level model object that the resource represents.

>>> serialize_docs(User, **data_template)
{ ... }

This of course only works if the data is derived from a model or queryset. As an added bonus, if a model instance (rather than the model class) is passed in, the generated docs could provide an example value.

Add option for merging a single related field

This is a common idiom:

template = {
    'fields': ['chr'],
    'key_map': {
        'chr': 'label',
    },
    'merge': True,
}

Which maps a single field on the related model and merges it into the parent model under the same name.

Support setting and option for mapping models to templates

Currently, templates for related models must be defined at the level in which they are traversed. This feature proposes being able to define a model โ†’ template mapping as an option or globally.

# In project settings.. mapping model label to preserialize options
PRESERIALIZE_MODEL_TEMPLATES = {
    'library.Library': { ... },
    'library.Book': { ... },
    'library.Author': { ... },
}

from preserialize.serialize import serialize

libraries = Library.objects.all()
# Internally this use the setting as it comes across model-based data
serialize(libraries)

Rename package to djangopreserialize

Hi, the package name is preserialize but as this is Django-specific, would it be possible to rename it to djangopreserialize matching PyPi (following PEP 8 (Package and Module Names) guidelines)? [I have a generic pre-serialization module I'd like to open source called preserialize and just checking first to avoid a package name clash].

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.