Skip to main content

Turn your API made with Django REST Framework(DRF) into a GraphQL like API.

Project description

django-restql

Build Status Latest Version Python Versions License

django-restql is a python library which allows you to turn your API made with Django REST Framework(DRF) into a GraphQL like API. With this you will be able to

  • Send a query to your API and get exactly what you need, nothing more and nothing less.

  • Control the data you get, not the server.

  • Get predictable results, since you control what you get from the server.

  • Save the load of fetching unused data from the server(Over-fetching and Under-fetching problem).

Isn't it cool?.

Installing

pip install django-restql

Querying Data

Using django-restql to query data is very simple, you just have to inherit the DynamicFieldsMixin class when defining a serializer.

from rest_framework import serializers
from django.contrib.auth.models import User

from django_restql.mixins import DynamicFieldsMixin

class UserSerializer(DynamicFieldsMixin, serializer.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'groups']

A regular request returns all fields as specified on DRF serializer, in fact django-restql doesn't handle this request at all:

GET /users

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "email": "yezileliilomo@hotmail.com",
        "groups": [1,2]
      },
      ...
    ]

django-restql handle all GET requests with query parameter, this parameter is the one used to pass all fields to be included in a response. For example to select id and username fields from user model, send a request with a query parameter as shown below.

GET /users/?query={id, username}

    [
      {
        "id": 1,
        "username": "yezyilomo"
      },
      ...
    ]

django-restql support querying both flat and nested resources, so you can expand or query nested fields at any level as long as your field is defined as nested field on a serializer. For example you can query a country and region field from location.

GET /users/?query={id, username, location{country, region}}

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "location": {
            "contry": "Tanzania",
            "region": "Dar es salaam"
        }
      },
      ...
    ]

django-restql got your back on querying iterable nested fields(one2many or many2many) too. For example if you want to expand groups field into id and name, here is how you would do it.

GET /users/?query={id, username, groups{id, name}}

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "groups": [
            {
                "id": 2,
                "name": "Auth_User"
            }
            {
                "id": 3,
                "name": "Admin_User"
            }
        ]
      },
      ...
    ]

If a query contains nested field without expanding and it's not defined as a nested field on a serializer, django-restql will return its id or array of ids for the case of nested iterable field(one2many or many2many). For example on a request below location is a flat nested field(many2one) and groups is an iterable nested field(one2many or many2many).

GET /users/?query={id, username, location, group}

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "location": 6,
        "groups": [1,2]
      },
      ...
    ]

Using fields=[..] and exclude=[..] kwargs

With django-restql you can specify fields to be included when instantiating a serializer, this provides a way to refilter fields on nested fields(i.e you can opt to remove some fields on a nested field). Below is an example which shows how you can specify fields to be included on nested resources.

from rest_framework import serializers
from django.contrib.auth.models import User

from django_restql.mixins import DynamicFieldsMixin

class BookSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title', 'author']


class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
    books = BookSerializer(many=True, read_only=True, fields=["title"])
    class Meta:
        model = Course
        fields = ['name', 'code', 'books']

GET /courses/

    [
      {
        "name": "Computer Programming",
        "code": "CS50",
        "books": [
          {"title": "Computer Programming Basics"},
          {"title": "Data structures"}
        ]
      },
      ...
    ]

As you see from the response above, the nested resource(book) has only one field(title) as specified on fields=["title"] kwarg during instantiating BookSerializer, so if you send a request like GET /course?query={name, code, books{title, author}} you will get an error that author field is not found because it was not included on fields=["title"] kwarg.

You can also specify fields to be excluded when instantiating a serializer by using exclude=[] as shown below

from rest_framework import serializers
from django.contrib.auth.models import Book, Course

from django_restql.mixins import DynamicFieldsMixin

class BookSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title', 'author']


class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
    books = BookSerializer(many=True, read_only=True, exclude=["author"])
    class Meta:
        model = Course
        fields = ['name', 'code', 'books']

GET /courses/

    [
      {
        "name": "Computer Programming",
        "code": "CS50",
        "books": [
          {"id": 1, "title": "Computer Programming Basics"},
          {"id": 2, "title": "Data structures"}
        ]
      },
      ...
    ]

From the response above you can see that author field has been excluded fom book nested resource as specified on exclude=["author"] kwarg during instantiating BookSerializer.

Note: fields=[..] and exclude=[] kwargs have no effect when you access the resources directly, so when you access books you will still get all fields i.e

GET /books/

    [
      {
        "id": 1,
        "title": "Computer Programming Basics",
        "author": "S.Mobit"
      },
      ...
    ]

So you can see that all fields have appeared as specified on fields = ['id', 'title', 'author'] on BookSerializer class.

Using return_pk=True kwargs

With django-restql you can specify whether to return nested resource pk or data. Below is an example which shows how we can specify fields to be included on nested resources.

from rest_framework import serializers
from django.contrib.auth.models import Book, Course

from django_restql.mixins import DynamicFieldsMixin

class BookSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title', 'author']


class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
    books = BookSerializer(many=True, read_only=True, return_pk=True)
    class Meta:
        model = Course
        fields = ['name', 'code', 'books']

GET /course/

    [
      {
        "name": "Computer Programming",
        "code": "CS50",
        "books": [1,2]
      },
      ...
    ]

So you can see that on a nested field books book pks have been returned instead of books data as specified on return_pk=True kwarg on BookSerializer.

Customizing django-restql

django-restql is very configurable, here is what you can customize on it.

  • Change the name of query parameter.

    If you don't want to use the name query as your parameter, you can inherit DynamicFieldsMixin and change it as shown below

    from django_restql.mixins import DynamicFieldsMixin
    
    class MyDynamicFieldMixin(DynamicFieldsMixin):
        query_param_name = "your_favourite_name"
    

    Now you can use this Mixin on your serializer and use the name your_favourite_name as your parameter. E.g

    GET /users/?your_favourite_name={id, username}

  • Customize how fields to include in a response are filtered. You can do this by inheriting DynamicFieldsMixin and override field methods as shown below.

    from django_restql.mixins import DynamicFieldsMixin
    
    class CustomDynamicFieldMixin(DynamicFieldsMixin):
        @property
        def fields(self):
            # Your customization here
            return fields
    

    Note: To be able to do this you must understand how django-restql is implemented, specifically DynamicFieldsMixin class, you can check it here. In fact this is how django-restql is implemented(just by overriding field method of a serializer, nothing more and nothing less).

Mutating Data(Creating and Updating Data)

django-restql got your back on creating and updating nested data too, it has two components for mutating nested data, NestedModelSerializer and NestedField. A serializer NestedModelSerializer has update and create logics for nested fields on the other hand NestedField is used to validate data before dispatching update or create.

Using NestedField & NestedModelSerializer to mutate data

Just like in querying data, mutating nested data with django-restql is very simple, you just have to inherit NestedModelSerializer on a serializer with nested fields and use NestedField to define those nested fields. Below is an example which shows how to use NestedModelSerializer and NestedField.

from app.models import Location, Amenity, Property
from django_restql.serializers import NestedModelSerializer
from django_restql.fields import NestedField


class LocationSerializer(NestedModelSerializer):
    class Meta:
        model = Location
        fields = ("id", "city", "country")


class AmenitySerializer(NestedModelSerializer):
    class Meta:
        model = Amenity
        fields = ("id", "name")


class PropertySerializer(NestedModelSerializer):
    location = NestedField(LocationSerializer)
    amenities = NestedField(AmenitySerializer, many=True)
    class Meta:
        model = Property
        fields = (
            'id', 'price', 'location', 'amenities'
        )

POST /api/property/

Request Body

{
    "price": 60000,
    "location": {
        "city": "Newyork",
        "country": "USA"
    },
    "amenities": {
        "add": [3],
        "create": [
            {"name": "Watererr"},
            {"name": "Electricity"}
        ]
    }
}

What's done here is pretty clear, location will be created and associated with the property created, also create operation on amenities will create amenities with values specified in a list and associate with the property, add operation will add amenity with id 4 to a list of amenities of the property.

Note: POST for many related field supports two operations which are create and add.


Response

{
    "id": 2,
    "price": 60000,
    "location": {
        "id": 3,
        "city": "Newyork",
        "country": "USA"
    },
    "amenities": [
        {"id": 1, "name": "Watererr"},
        {"id": 2, "name": "Electricity"},
        {"id": 3, "name": "Swimming Pool"}
    ]
}

PUT /api/property/2/

Request Body

{
    "price": 50000,
    "location": {
        "city": "Newyork",
        "country": "USA"
    },
    "amenities": {
        "add": [4],
        "create": [{"name": "Fance"}],
        "remove": [3],
        "update": {1: {"name": "Water"}}
    }
}

Note: Here add, create, remove and update are operations, so add operation add amenitiy with id 4 to a list of amenities of the property, create operation create amenities with values specified in a list, remove operation dessociate amenities with id 3 from a property, update operation edit amenity with id 1 according to values specified.

Note: PUT/PATCH for many related field supports four operations which are create, add, remove and update.


Response

{
    "id": 2,
    "price": 50000,
    "location": {
        "id": 3,
        "city": "Newyork",
        "country": "USA"
    },
    "amenities": [
        {"id": 1, "name": "Water"},
        {"id": 2, "name": "Electricity"},
        {"id": 4, "name": "Bathtub"},
        {"id": 5, "name": "Fance"}
    ]
}


Using NestedField with accept_pk=True kwarg.

accept_pk=True is used if you want to update nested field by using pk/id of existing data(basically associate and dessociate existing nested resources with the parent resource without actually mutating the nested resource). This applies to ForeignKey relation only.

from app.models import Location, Amenity, Property
from django_restql.serializers import NestedModelSerializer 
from django_restql.fields import NestedField


class LocationSerializer(NestedModelSerializer):
    class Meta:
        model = Location
        fields = ("id", "city", "country")


class PropertySerializer(NestedModelSerializer):
    location = NestedField(ocationSerializer, accept_pk=True)
    class Meta:
        model = Property
        fields = (
            'id', 'price', 'location'
        )

POST /api/property/

Request Body

{
    "price": 40000,
    "location": 2
}

Note: Here location resource with id 2 is already existing, so what's done here is create new property resource and associate it with a location with id 2.

Response

{
    "id": 1,
    "price": 40000,
    "location": {
        "id": 2,
        "city": "Tokyo",
        "country": "China"
    }
}

Using NestedField with create_ops=[..] and update_ops=[..] kwargs.

You can restrict some operations by using create_ops and update_ops keyword arguments as follows

from app.models import Location, Amenity, Property
from django_restql.serializers import NestedModelSerializer 
from django_restql.fields import NestedField


class AmenitySerializer(NestedModelSerializer):
    class Meta:
        model = Amenity
        fields = ("id", "name")


class PropertySerializer(NestedModelSerializer):
    amenities = NestedField(
        AmenitySerializer, 
        many=True,
        create_ops=["add"],  # Allow only add operation(restrict create operation)
        update_ops=["add", "remove"]  # Allow only add and remove operations(restrict create and update operations)
    )
    class Meta:
        model = Property
        fields = (
            'id', 'price', 'amenities'
        )

POST /api/property/

Request Body

{
    "price": 60000,
    "amenities": {
        "add": [1, 2]
    }
}

Note: According to create_ops=["add"], you can't use create operation in here!.

Response

{
    "id": 2,
    "price": 60000,
    "amenities": [
        {"id": 1, "name": "Watererr"},
        {"id": 2, "name": "Electricity"}
    ]
}

PUT /api/property/2/

Request Body

{
    "price": 50000,
    "amenities": {
        "add": [3],
        "remove": [2]
    }
}

Note: According to update_ops=["add", "remove"], you can't use create or update operation in here!.

Response

{
    "id": 2,
    "price": 50000,
    "amenities": [
        {"id": 1, "name": "Water"},
        {"id": 3, "name": "Bathtub"}
    ]
}

Running Tests

python setup.py test

Credits

  • Implementation of this library is based on the idea behind GraphQL.
  • My intention is to extend the capability of drf-dynamic-fields library to support more functionalities like allowing to query nested fields both flat and iterable at any level and allow writing on nested fields while maintaining simplicity.

Contributing PRs Welcome

We welcome all contributions. Please read our CONTRIBUTING.md first. You can submit any ideas as pull requests or as GitHub issues. If you'd like to improve code, check out the Code Style Guide and have a good time!.

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

django-restql-0.5.0.tar.gz (12.1 kB view details)

Uploaded Source

Built Distribution

django_restql-0.5.0-py3-none-any.whl (13.0 kB view details)

Uploaded Python 3

File details

Details for the file django-restql-0.5.0.tar.gz.

File metadata

  • Download URL: django-restql-0.5.0.tar.gz
  • Upload date:
  • Size: 12.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.11.1 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.6.8

File hashes

Hashes for django-restql-0.5.0.tar.gz
Algorithm Hash digest
SHA256 fd01571610abb5d7d20819f38e6b35d04406d28d2d206011b63745aef41b6b73
MD5 449c18d80f94cdc976e01a034f8c82b0
BLAKE2b-256 759e6f83a3c892479854cdd32c3c44650e8f5d6667aabd0823ddd5af35f79a8e

See more details on using hashes here.

File details

Details for the file django_restql-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: django_restql-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 13.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.11.1 setuptools/40.8.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.6.8

File hashes

Hashes for django_restql-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6dfd5c0109090eab6d1acbb3a7afe274791d011e688a3cbc1474c141930827a9
MD5 0948c0229003c565d6f0f5238bef540f
BLAKE2b-256 aa9e934df43d81836922a65043af8af36958469f32b235d96438399cb99de7f2

See more details on using hashes here.

Supported by

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