django-universal-api 0.4.2
Automatic universal CRUD APIs for your models.
License: MIT
Requires Python: >=3.6
Keywords: Django,API,CRUD

# Django Universal API
Automatic universal CRUD APIs for your models.��

Version: 0.4.2 is out!✨  This package requires Python >= 3.6, and Django >= 2.0.

**New in 0.4.2:**

1. Fixed #1 - improper handling of malformed URL.

<details>
<summary>Change Log</summary>
**New in 0.4.1:**

1. Added support for `PATCH` method.
2. Fixed missing `content-type` for some end-points.
3. Existence of response body now follows conventions for each HTTP method.

**New in 0.4.0:**

1. Better security: chained look-up respects `SerializableModel` declarations.
2. `GET` requests on endpoints that are meant for a single record no longer returns a list.
</details>

[![PyPI version](https://badge.fury.io/py/django-universal-api.svg)](https://badge.fury.io/py/django-universal-api)

---

Tired of writing those serialization methods and URL rules? Just want a simple set of APIs for tweaking with your models? Django Rest Framework being too fancy for you?

Welcome to Django Universal API, a Django app that automatically adds simple CRUD APIs and a powerful search API to your existing models. No coding required! (almost)

---
Now available on PyPI: `pip install django-universal-api`, built with [Poetry](https://github.com/python-poetry/poetry).

## Features

- Automatic REST-like API generation
- Minimal configuration required
- Expressive search
- Customizable behavior

## Quick Start
First install Django Universal API with pip, `pip install django-universal-api`.
Then register Django Universal API app in your Django project's settings file. Modify the `settings.py` to look like this:

```python
INSTALLED_APPS = [
    'django_universal_api',
    # ...
]
```
Please note that the app name is slightly different from the PyPI package name. (Underscore vs. dash)

Don't forget to configure a top level URL name for Django Universal API app too. Let's use `universal` for this example. Configure your project's URL setting as follows, by using `include`:

```python
from django.urls import path
from django.urls import include

urlpatterns = [
    path('universal/', include('django_universal_api.urls')),
]
```



The most difficult part is over. Now you just need to tell us which models need access to the universal APIs.
Because API generation requires explicit declaration, you must tag each model by subclassing from the `SerializableModel` class.

Then simply add `serializable_attributes`, a list of attribute names to your model, and you are good to go.

```python
from django.db import models
from django_universal_api import SerializableModel


class Stuff(models.Model, SerializableModel):
    weight = models.IntegerField()
    size = models.IntegerField()
    name = models.CharField(max_lenght=50)
    owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
    
    serializable_attributes = ["weight", "size"]
```

By doing so, you tell the Universal API app that this model is open for API access. And when it comes to GET requests, only the attributes specified will be returned.

You can also specify arbitrary attributes by specifying the `additional_attributes` in the model.

```python
from django.db import models
from django_universal_api import SerializableModel


class Owner(models.Model, SerializableModel):
    username = models.CharField(max_length=50)
    
    serializable_attributes = ["username"]

class Stuff(models.Model, SerializableModel):
    weight = models.IntegerField()
    size = models.IntegerField()
    name = models.CharField(max_length=50)
    owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
    
    serializable_attributes = ["weight", "size"]
    additional_attributes = {
      "owner_name": "owner__username"
    }
```

As you can see, you can chain up attribute look-ups in `additional_attributees`, giving you more freedom in choosing what to return. 

For all your hard work, you get the following nicely looking URLs for manipulating your model:
```
GET universal/api/Stuff/1
PUT universal/api/Stuff/1
PATCH universal/api/Stuff/1
POST universal/api/Stuff/1
DELETE univeresal/api/Stuff/1
GET univeresal/api/Stuff/size/55  # searching for Stuff that have a size of 55
POST universal/api/Stuff
POST universal/api/Stuff/search  # more advanced search functionality
```
These URLs are configured automatically, and relies on the name of your models for naming. The model names are case-sensitive for now.

## Automatic API Endpoints

Django Universal API automatically generates API endpoints for models, and the generation process follows the following rules:

1. The model must a subclass of `SerializableModel`, or no endpoint is generated.
2. The model class must define a field called `serializable_atrributes` to specify what data fields of the model are exposed by the API endpoints, the default is nothing.
3. All payload are in the format of JSON, for both requests and responses.
4. Model names are case sensitive in URLs.



Here are all the API endpoints Django Universal API creates, when there is at least one model being a subclass of `SerializableModel`.

| HTTP Method |                Endpoint                 |                         Explanation                          |
| :---------: | :-------------------------------------: | :----------------------------------------------------------: |
|     GET     |        `api/{model_name}/{uri}`         |  Retrieve an item with ID `uri` from the model `model_name`  |
|    POST     |        `api/{model_name}/{uri}`         |   Update an item with ID `uri` from the model `model_name`   |
|     PUT     |        `api/{model_name}/{uri}`         |   Update an item with ID `uri` from the model `model_name`   |
|   DELETE    |        `api/{model_name}/{uri}`         |   Delete an item with ID `uri` from the model `model_name`   |
|     GET     |           `api/{model_name}`            |          List all items from the model `model_name`          |
|    POST     |           `api/{model_name}`            |          Create an item with the model `model_name`          |
|   OPTIONS   |           `api/{model_name}`            |                           Reserved                           |
|    POST     |        `api/{model_name}/search`        |    Performs searches on items from the model `model_name`    |
|     GET     | `api/{model_name}/{field_name}/{value}` | Search for items from model `model_name` whose field `field_name` has a value of `value` |

**Special Behavior**

Some of Django Universal API's automatically generated API endpoint do not behave perfectly RESTful:

- `PUT`  request to the `api/<model_name>/<uri>` endpoint allows partial payloads (the payload for the `PUT` request does not need to have all fields of the model).
- `POST` is used to updated a model.

## Search

There are two ways to do search with Django Universal API. The easiest way is to make use of the `api/{model_name}/{field_name}/{value}` endpoint. Please note that this method only supports searching with one field and one value.

For example, if you have a model named `Book` and one of the field for the model is `author`. You can search for all books who's author is Bob by sending a `GET` request to

```
api/Book/author/Bob
```

In the response body you will see a list of Book objects that matches the search criteria.

You can also make use of the more expressive `api/{model_name}/search` endpoint, which requires you to send a search payload via `POST`.

A sample search request looks like this:

```
POST api/Book/search

{
	"page": 1,
	"page_size": 20,
	"order_by": "id",
	"search_keys": {
		"author__icontains": "bob",
		"year_published__gte": 2001
	}
}
```

Some of the parameters are optional and can be omitted:

| Name        | Required | Explanation                                                  |
| ----------- | -------- | ------------------------------------------------------------ |
| page        | optional | Integer. The page number of the search result you want to access. |
| page_size   | optional | Integer. The page size for the pagination engine.            |
| order_by    | optional | String. The name of the field you want the result be ordered by. You can include a `-` sign in front of the filed name to indicate reverse order. |
| search_keys | required | Object. It contains key-value pairs of the search parameters. The keys must be field names of the model. It is also allowed for the keys to have Django style lookups. (As of now the entirety of the `search_keys` is fed into the model manager's `filter()` method.) |



## Settings
Django Universal API provides sensible default settings for most things. However you also have the freedom to change how the APIs behave by supplying your own settings in your Django project's `settings.py` file.

You can change some of the behaviors of the Universal API app by including a setting entry named `UNIVERSAL_API_SETTINGS` in your project's settings file. The following settings are currently supported:

```python
UNIVERSAL_API_SETTINGS = {
    "SERIALIZATION": {
        "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S",   # format for Datetime serialization
        "DATE_FORMAT": "%Y-%m-%d",   # format for Date serialization
        "TIME_FORMAT": "%H:%M:%S",   # format for Time serialization
    },
    "PAGINATION": {
        "PAGE_SIZE": 50,   # page size of paginated results (listings, searches)
    },
    "AUTH_FUNCTION": lambda u: u.is_authenticated  # must be a function that takes a User object as input
}

```

You can control the serialization format of time related data types, pagination settings and custom API authentication.

## Serialization
Serialization of output and input data is handled automatically for most common data types. The most sensible way of serialization is chosen. You can set the serialization behavior of time related data in your project settings. For more information please refer to the Settings section of this page.

ForeignKey objects are serialized to their primary keys by default. This applies for both ways of communication. This means if you want to create an object that references another object, you have to pass in the referenced object's primary key for the API call to be successful. 

User objects in POST requests are serialized by matching their usernames (i.e. you should POST the username instead of the primary key). This behavior is chosen because it is often easier to obtain a logged-in user's username in the front-end.

## Common HTTP Status Codes

Django Universal API uses HTTP status codes together with the reason phrase to communicate errors. All errors do not have a body in the HTTP response. Here are some common errors that are returned by Django Universal API:

| Status Code |                      Reason                       |                         Explanation                          |
| :---------: | :-----------------------------------------------: | :----------------------------------------------------------: |
|     400     |                   Invalid JSON                    |     Django Universal API cannot parse your JSON payload.     |
|     400     |                    Invalid URI                    |              The URI you specified is invalid.               |
|     400     |                Invalid Parameters                 |           The parameters you specified is invalid.           |
|     400     |                Invalid Foreign Key                |          The foreign key you specified is invalid.           |
|     400     |               Value Has Wrong Type                | The value you specified does not have the type Django Universal API is expecting. |
|     400     |                  Illegal Access                   | Your chained look-up is trying to access a model that is not allowed. |
|     404     |               Record Does Not Exist               | The record you requested does not exist, or is not allowed to be accessed. |
|     404     |               Model Does Not Exist                | The model you requested does not exist, or is not allowed to be accessed. |
|     460     | Model Does Not Support Serialization [Deprecated] |   The model you requested does not support serialization.    |
|     403     |                     Forbidden                     | You are not logged in, or you did not supply the correct CSRF token. |
|     405     |                Method Not Allowed                 |       The HTTP method is not allowed on this endpoint.       |

**Special Behavior**

Please note that the `460` status code is used by Django Universal API.

## Custom Behaviors
By default, the Update API will simply update the object's attributes with values provided in the API call. You can modify this behavior by adding custom properties in your model. For example, consider the following code:

```python
from django.db import models
from django_universal_api import SerializableModel


class Owner(models.Model, SerializableModel):
    name = models.CharField(max_length=64)
    user = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL)
    serializable_attributes = ["id", "name", "password_hook"]

    @property
    def password_hook(self):
        return "********"

    @password_hook.setter
    def password_hook(self, value):
        if not self.user:
            user = User.objects.create_user(username=self.company_name, password=value)
            user.save()
            self.user = user
        else:
            user = self.user
            user.set_password(value)
        user.save()

    @password_hook.deleter
    def password_hook(self):
        pass
```

Here, we added some rather complex logic to update the Owner object's user's password. Notice we included `password_hook`, the name of the property in `serializable_attributes`. This is not required for the PUT or POST request to work, the requests can simply pass in a value for password_hook for the user creation logic to take place. This is included only to help the front-end developer to figure out what hidden "magic" attributes are available.

**Special Behavior**

Also, attributes that ends with the `_hook` suffix will always be written only after the object has been saved at least once. This means that hooks are also a good place to put logic that will update a many-to-many relationship field, since they cannot be updated until the model object has been saved. For now, many-to-many relationship fields do not receive special treatment from the framework.


## Why you shouldn't use it for production

In case you haven't figured out why it is a bad idea to use this in a production environment, here are some compelling
reasons:

- Access to models are not strictly enforced by declaration if field look-up chaining is allowed
- Hiding a field does not mean it will never be changed through filed look-up chaining