This library allows a django website to add an api quickly and easily.
api is a library to add an API to any Django website. It has been inspired by the AWS API - control and security are a big part. Once set up, you can add methods to your site with minimal code - in yourapp/api.py:
@api.method
def manipulate_something(rl:Role, message:str, thing:MyModel)->Result:
# manipulate thing
return Result(...)
This makes yourapp/manipulate_something available in your site's API. Your site's API is called using POSTs, so any language should be able to call it easily (POST to https://yoursite/api/yourapp/manipulate_something). You can post from your site's web pages, securing them using {% csrf_token %}
as normal. For Python clients there is a helper library, api_api
, which wraps your API as ordinary Python method calls. Any methods you define can be called from your site's code well as through your site's API. When called from your code, security is checked using the Role
you pass in, just the same as for an API call.
[aside: making an API trivial to write and use has meant we now take an API-first approach to developing my web sites, removing all POST processing from the UI views (we now never use Django forms). In particular, we have found: creating a dynamic/instant response UI is simple; no more 'are you sure you want to post that again' for our sites' users; testing is much more straightforward (we are now a convert to writiing automated tests); our sites' APIs are not an add-on, so don't have gaps]
Your site's API will be able to use values which are:
str
, int
, float
, bool
, bytes
, None
Enum
s of simple valuesmodel.Model
s. Sometimes called a resource, they will need a little preparation for use in your API@dataclass
sA model.Model
, or resource is given in an API either by an id (eg 'u-0123456789abcdef'), or a urn (eg 'urn/home/user/fredbloggs'). A resource's id is fixed and is made from its primary key, and its urn is a path to find the resource - a urn must only match one resource. All ids and urns are unique on your site - no two resources, no matter what type, will have the same one. Your model needs a little preparation, see here for details. This, amongst other things, sets the id prefix ('i' in 'i-012..'), and urn tag ('home/user' in 'urn/home/user/fre..'). When ready, your resource can be used in your api:
@api.Method
def find_repo(rl:Role, user:User, name:str)->Repo:
return Repo.objects.find(user=user, name=name)
Sometimes, you may need to convert between instances and ids - once prepared, your model will have .urn
, .api_id
and .api_find()
. Here we're calling the api of another instance in our website:
# tell my other instance to create the repo...
instance.api.repo.create_repo(repo=repo.api_id)
api_api is .api_id
-aware, so this also works
instance.api.repo.create_repo(repo=repo)
File uploads are very easy - use api_api.FileUpload
as a parameter:
from api_api import FileUpload
@api-method
def receive_file(rl:Role, file:FileUpload):
...
The file
parameter will be a FileUpload
, which has .name
, .file
and .content_type
properties. Sending a FileUpload
is as simple as adding a <input type="file"..
to the form on your web page, or passing in a FileUpload
to your method in api_api.
Security is a big part of any API. The key parts are:
Policy
s - these describe what's allowed (api calls) to what (objects / rows in tables). Policy
s can also be attached to objects to provide additional control.Role
s - these connect who to a series of Policy
s. A Role
can inherit permission Role
s this could be used allow, say, temprary, restricted access to otherwise inaccessible objects - in effect, at setuid-like ability.Key
s - these deintify the Role
making the call.api needs help from your code:
api.Mixin
and api configuration. More details hereUser
model needs to be api-ready and to provide a default_user
. You will need to replace the Django User
model with your own. Django expects this - there is a recipe to follow. However, it is much easier before any User
s have been added. Details here.api.py
in each of your project's applications for that application's api methods. More details hereYou may need pages on your site for users to manage Policy
s, Role
s and Key
s.
Install the library:
pip install api_svnplace
settings.py
Add api
to your site's installed apps. The exact position isn't critical, somewhere after the django apps.
INSTALLED_APPS = [
...
'api',
]
Add api
's middlewhere late on in the middleware list, after authentication and security:
MIDDLEWARE = [
...
'api.middleware.APIMiddleware',
]
Add api to your URLs:
urlpatterns = [
...
path('api/', include('api.urls', namespace='api')),
...
]
Add API_ settings:
# API
# Needs to be between 4 and 56 bytes long
# Set your own and keep it secret
API_ID_ENCRYPTION_KEY = b'secret secret secret secret'
# Needs to be 8 bytes long
# Set your own and keep it secret
API_ID_INITIALISATION_VECTOR = b'>secret<'
User
ModelWhich application provides the replacement User
model is not important - in the examples the application home
has been used.
In settings.py
:
AUTH_USER_MODEL="home.User"
and in home/models.py
:
from django.contrib.auth.models import AbstractUser
import api
class User(AbstractUser, api.Mixin):
# api support...
class API:
urn_prefix = 'home/user'
api_id_prefix = 'u-'
textname = 'user'
@property
def owning_user(self):
return self
@property
def urn_id(self):
return f'{self.username}'
@staticmethod
def urn_find(username):
return User.objects.get(username=username)
@property
def default_role(self):
role, created = self.roles.get_or_create(name='')
if created:
role.role_policies.add(home.allow_everything)
return role
# ...api support
For more on getting a model ready for use in the api see here.
Sometimes your site needs to own things. The easiest way to achieve this is to have a User
set aside just for this purpose - the Site User.
in home/__init__.py
:
default_app_config = 'home.apps.HomeConfig'
site_user = None
in home/apps.py
:
from django.apps import AppConfig
from django.contrib import auth
from django.db import OperationalError
import home
class HomeConfig(AppConfig):
def ready(self) -> None:
super().ready()
try:
UserModel = auth.get_user_model()
home.site_user = UserModel.objects.get(username='svnplace')
except (OperationalError, UserModel.DoesNotExist):
# this allows MakeMigrations etc before the tables have been created
pass
This Policy
allows anything on anything. Its text defines it:
statements:
-
actions: "*"
allow: true
resources: "*"
Somewhere in your code:
from api.models import Policy
home.allow_everything, _ = Policy.objects.update_or_create(
user=home.site_user,
name='Allow Everything',
defaults={
'text':'''statements:
-
actions: "*"
allow: true
resources: "*"
''',
'public':True
},
)
This could be in HomeConfig.ready()
, but that makes reloading a whole database with Django difficult. My approach was to separated out the site preparation code into a ready_site()
method called from a ./manage.py readysite
, a new command I added to my own site. Details here.
in home/__init__.py
:
default_app_config = 'home.apps.HomeConfig'
site_user = None
allow_everything = None
In home/apps.py
:
class HomeConfig(AppConfig):
def ready(self) -> None:
...
from api.models import Policy
try:
home.allow_everything = Policy.objects.get(name='Allow Everything')
except (OperationalError, Policy.DoesNotExist):
# this allows MakeMigrations etc before the tables have been created
pass
Create an api.py
in any applications which need an api:
home
...
admin.py
api.py
apps.py
...
Inside api.py
add your methods:
from dataclasses import dataclass
import api
from api.models import Role
@dataclass
class SiteDescription:
name: str
@api.method:
def describe_site(rl:Role)->SiteDescription:
return SiteDescription(name='My wonderful site')
This is how to create and print a key and its secret:
from api.models import Key
key = Key(role=user.default_role)
secret = key.create_secret()
key.save()
print(key.api_id)
print(secret.decode())
This code will get you going, although you will want a way for your users to manage their keys.
Prepare by storing your credentials:
in ~/.mywonderfulsite/config
:
[default]
access_key_id = k-0123456789abcdef
access_key_secret = abcdefghijklmnopqrstABCDEFGHIJKLMNOPQRST
access_key_id
is key.api_id
from here and access_key_secret
is secret.decode()
.
Call your api methods:
from api_api import API
api = API('http://mywonderfulsite.com/api', ['~/.mywonderfulsite/config'])
site_description = api.home.describe_site()
This example shows calling your API using Python. The call is made using an HTTP POST, and the result is returned as json. This makes it simple to embed api calls as <form>
s inside your web pages, and to call your api from your clients' languages of choice.
More details can be found here
You may need to use your models in your API. Each model will need a little preparation. The details can be found here.
Details to write methods can be found here
A guide to create a ready_site
command for your project can be found here.