API resources
This file handles the CRUD operations.
When you create a new API called Swords
, the resource
name is Sword
and the file name is SwordResource
. The endpoint for CRUD operations are:
Action | HTTP method | Endpoint |
---|---|---|
Create | POST | /swords |
List | GET | /swords |
Detail | GET | /swords/:id |
Update | PUT | /swords/:id |
Delete | DELETE | /swords/:id |
If your resource belongs to a parent model, for example a sword belongs to a team, then your
endpoints will be the following (note that swords for the create and list endpoints are nested
under the teams
resource:
Action | HTTP method | Endpoint |
---|---|---|
Create | POST | /teams/:id/swords |
List | GET | /teams/:id/swords |
Detail | GET | /swords/:id |
Update | PUT | /swords/:id |
Delete | DELETE | /swords/:id |
Collection
This method is executed when you make a request for a list of a specific resource.
Example: GET /swords
.
Create a file called SwordResource
that is a subclass of 1 of the following:
GenericResource
: subclass this class if you want to execute arbitrary codeDatabaseResource
: subclass this class only if your resource is directly associated to a database model. This subclass has many methods defined by default that handles CRUD operations automatically.
Creating a subclass of GenericResource
Create a file called backend/api/resources/SwordResource.py
:
from .GenericResource import GenericResource
import requests
class SwordResource(GenericResource):
@classmethod
def collection(self, query, meta, user, **kwargs):
"""
query:
a dictionary of URL query parameters
meta:
a dictionary of meta parameters from the URL; such as _limit and _format
user:
a user object that represents the current user who is making the API call
kwargs:
a dictionary of other values that are passed down through the API request lifecycle; such
as parent_model, etc.
"""
# Fetch a list of Swords from the internet
response = requests.get('https://www.dropbox.com/s/mnjsxzdsyc39hr3/IkeaStorageJson.json')
return self.build_result_set(response['swords'], user, **kwargs)
Creating a subclass of DatabaseResource
from .DatabaseResource import DatabaseResource
from examples.models import Sword
class SwordResource(DatabaseResource):
# Set this class variable to the model that the resource is associated with
model_class = Sword
"""
Optionally override collection because the DatabaseResource will handle limits
and pagination automatically. If you don’t override the collection method, DatabaseResource
will automatically fetch a list of models from the database for you.
"""
@classmethod
def collection(self, query, meta, user, **kwargs):
"""
Use collection to handle pre-filtering of the results from the database.
"""
# Filter swords that are weak and belong to the current team
team = kwargs['parent_model']
return Sword.objects.filter(team_id=team.id, weak=False)
Here is how we would get a list of swords that are weak and belong to team ID 1:
user = User.objects.get(id=3)
team = Team.objects.get(id=1)
# You typically don’t have to call this function by itself.
# This function is called by making a request to /teams/1/swords.
swords = SwordResource.collection({}, {}, user, parent_model=team)
Create
This method is executed when you make a request to create a new instance of a resource.
Example: POST /swords
.
GenericResource
from services.datadog import increment
class SwordResource(GenericResource):
# code from before
@classmethod
def create(self, payload, user, **kwargs):
"""
payload:
a dictionary of the request body
For example, if your request body is { "sword" : { "weak": false } },
then your payload will be the following dictionary: { 'weak': False }.
NOTE:
Payloads must be nested inside a key named after the singular form of the resource name.
user:
a user object that represents the current user who is making the API call
kwargs:
a dictionary of other values that are passed down through the API request lifecycle;
such as parent_model, etc.
"""
# Increment a create metric in DataDog
increment('swords.create')
if payload.get('weak'):
name = 'Weak sword'
else:
name = 'Strong sword'
# Always return a new instance of the resource
return self({ 'name': name }, user, **kwargs)
DatabaseResource
You typically never have to override the create
class method. The instances where it is overrided
include a scenario when you want to execute a callback function only if the resource was
successfully created.
The DatabaseResource
create
method will take the keys and values in the payload argument, assign
those values to a newly instantiated Sword
object, and then save the model to the database.
class SwordResource(DatabaseResource):
# code from before
@classmethod
def create(self, payload, user, **kwargs):
def _upload(resource):
# upload something only if the resource was successfully created
self.on_create_callback = _upload
return super().create(payload, user, **kwargs)
Member
This method is executed when you make a fetch a single resource using it’s ID.
Example: GET /swords/:id
.
GenericResource
class SwordResource(GenericResource):
# code from before
@classmethod
def member(self, pk, user, **kwargs):
"""
pk:
a string or integer from the URL parameter :id
user:
a user object that represents the current user who is making the API call
kwargs:
a dictionary of other values that are passed down through the API request lifecycle; such
as parent_model, etc.
"""
# Fetch 1 of Sword from the internet
array = requests.get('https://www.dropbox.com/s/mnjsxzdsyc39hr3/IkeaStorageJson.json')
sword = filter(lambda x: x['id'] == pk, array)
return self({ 'name': sword['name'], 'weak': False }, user, **kwargs)
DatabaseResource
You almost never have to override the member
method.
If you do, see TrainingRunResource
for an example.
Update
This method is executed when you want to update a single resource using it’s ID.
Example: PUT /swords/:id
.
This is almost the same as the create
method except it’s an instance method and the ID of the
resource you want to update must be included in the URL.
class SwordResource(GenericResource):
# code from before
def update(self, payload, **kwargs):
"""
payload:
a dictionary of the request body
For example, if your request body is { "sword" : { "weak": false } },
then your payload will be the following dictionary: { 'weak': False }.
NOTE:
Payloads must be nested inside a key named after the singular form of the resource name.
kwargs:
a dictionary of other values that are passed down through the API request lifecycle; such
as parent_model, etc.
"""
print('Awesome update!')
# Always return self
return self
DatabaseResource
You almost never have to override the update
method.
If you do, see ModelVersionResource
for an example.
Delete
This method is executed when you want to delete a single resource using it’s ID.
Example: DELETE /swords/:id
.
class SwordResource(GenericResource):
# code from before
def delete(self, **kwargs):
"""
kwargs:
a dictionary of other values that are passed down through the API request lifecycle; such
as parent_model, etc.
"""
print('Deleted!')
DatabaseResource
You almost never have to override the delete
method.
Registering parent models
If your resource belongs to a parent model, for example a sword belongs to a team, you must do the following in order for these endpoints to work properly:
- Create:
POST /teams/:id/swords
- List:
GET /teams/:id/swords
import importlib
class SwordResource(DatabaseResource):
# code from before
SwordResource.register_parent_resource(
getattr(importlib.import_module('api.resources.TeamResource'), 'TeamResource'),
)
Pre-load single associated object
The sword model belongs to a user via the sword.user_id
column. If you are fetching a list of 100
swords and in your code you are accessing the methods of the user
object via sword.user
(e.g. sword.user.name
), you will inadvertently make 100 extra API calls to the users
database table. Do the following to prevent this:
import importlib
class SwordResource(DatabaseResource):
# code from before
SwordResource.register_collective_loader_find(
getattr(importlib.import_module('api.resources.UserResource'), 'UserResource'),
)
Pre-load multiple associated objects
The sword has many opponents that were defeated by it. You can query all the opponents via the
swords.opponents.objects.all()
method. This will fetch all opponents from the database where
opponent.sword_id
equals sword.id
. If you are accessing the opponents of each sword when
fetching a list of 100 swords, you will be making 100 extra API calls to the opponents
database.
Do the following to prevent this:
import importlib
class SwordResource(DatabaseResource):
# code from before
SwordResource.register_collective_loader_select(
getattr(importlib.import_module('api.resources.OpponentResource'), 'OpponentResource'),
)