Skip to content

Instantly share code, notes, and snippets.

@selimslab
Last active June 7, 2020 18:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save selimslab/deb13d127776e551ee58d6c9ff293108 to your computer and use it in GitHub Desktop.
Save selimslab/deb13d127776e551ee58d6c9ff293108 to your computer and use it in GitHub Desktop.
an API to explore mobile user events , built with Django REST, has filter, group, sort, and aggregate

Examples

Show the number of impressions and clicks that occurred before the 1st of June 2017, broken down by channel and country, sorted by clicks in descending order.

/api/v1/records/?date_before=2017-06-01&group_by=channel,country&sum=impressions,clicks&ordering=-clicks

Show the number of installs that occurred in May of 2017 on iOS, broken down by date, sorted by date in ascending order.

/api/v1/records/?os=ios&date_after=2017-05-01&date_before=2017-05-31&group_by=date&sum=installs&ordering=date

Show revenue, earned on June 1, 2017 in US, broken down by operating system and sorted by revenue in descending order

/api/v1/records?country=US&date=2017-06-01&group_by=os&sum=revenue&ordering=-revenue

Show CPI and spend for Canada (CA) broken down by channel ordered by CPI in descending order. Please think carefully which is an appropriate aggregate function for CPI.

/api/v1/records/?country=CA&group_by=channel&sum=cpi&ordering=-cpi

fields

"date", "channel", "country", "os", "impressions", "clicks", "installs", "spend", "revenue", "cpi"

API Usage

filtering

filter by time range, channels, countries, operating systems

/api/v1/records?os=ios

/api/v1/records/?os=ios&date_after=2017-05-01&date_before=2017-05-31

grouping

parameter: group_by

"date", "channel", "country", or "os"

/api/v1/records?group_by=os

separate multiple arguments by ","

/api/v1/records?group_by=os,country

sum

aggregate records

/api/v1/records?group_by=date&sum=installs,clicks

sort

order by any field

parameter: ordering

/api/v1/records?ordering=date

use - for descending

/api/v1/records?ordering=-date

quick start

python manage.py makemigrations api
python manage.py migrate
python manage.py runserv

docs

swagger docs at /docs

from django_filters.rest_framework import FilterSet
from django_filters import DateFromToRangeFilter
from api.models import Record
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
class CustomFilter(FilterSet):
date = DateFromToRangeFilter()
class Meta:
model = Record
fields = ("channel", "country", "os", "date")
from django.db import models
class Record(models.Model):
date = models.DateField()
channel = models.TextField()
country = models.CharField(max_length=3)
os = models.CharField(max_length=10)
impressions = models.PositiveIntegerField()
clicks = models.PositiveIntegerField()
installs = models.PositiveIntegerField()
spend = models.FloatField()
revenue = models.FloatField()
from rest_framework.serializers import (
ModelSerializer,
FloatField,
SerializerMethodField,
)
from api.models import Record
class DynamicFieldsSerializer(ModelSerializer):
""" Enables dynamic serializer fields """
def __init__(self, *args, **kwargs):
fields = kwargs.pop("fields", set())
super().__init__(*args, **kwargs)
if fields and "__all__" not in fields:
all_fields = set(self.fields.keys())
for not_requested in all_fields - set(fields):
# Drop any fields that are not specified in the `fields` argument.
self.fields.pop(not_requested)
class RecordSerializer(DynamicFieldsSerializer):
cpi = FloatField(allow_null=True)
class Meta:
model = Record
fields = (
"date",
"channel",
"country",
"os",
"impressions",
"clicks",
"installs",
"spend",
"revenue",
"cpi",
)
from django.urls import path
from rest_framework.routers import DefaultRouter
from django.conf.urls import include
from api.views import RecordViewSet
router = DefaultRouter()
router.register(r"records", RecordViewSet, basename="Record")
urlpatterns = [
path("", include(router.urls)),
]
from django.contrib import admin
from django.urls import path
from django.urls import include
from rest_framework_swagger.views import get_swagger_view
docs_view = get_swagger_view(title="API Docs")
urlpatterns = [
path("admin/", admin.site.urls),
path("api/v1/", include("api.urls")),
path("docs/", docs_view),
]
from typing import Iterable
from rest_framework import filters, viewsets
from rest_framework.response import Response
from django.db.models import Sum, FloatField
from django_filters.rest_framework import DjangoFilterBackend
from api.serializers import RecordSerializer
from api.filters import CustomFilter
from api.models import Record
class RecordViewSet(viewsets.ModelViewSet):
serializer_class = RecordSerializer
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filter_class = CustomFilter
allowed_fields = {
"date",
"channel",
"country",
"os",
"impressions",
"clicks",
"installs",
"spend",
"revenue",
"cpi",
}
allowed_groups = {"date", "channel", "country", "os"}
fields = allowed_fields - {"cpi"} # default fields, without cpi
queryset = Record.objects.all()
def list(self, request) -> Response:
fields_to_group_by = self.request.query_params.get("group_by", None)
self.group(fields_to_group_by)
fields_to_sum = self.request.query_params.get("sum", None)
self.aggregate(fields_to_sum)
# filter
self.queryset = self.filter_queryset(self.queryset)
# init the serializer with dynamic fields
serializer = RecordSerializer(self.queryset, many=True, fields=self.fields)
return Response(serializer.data)
def group(self, fields_to_group_by: str):
if fields_to_group_by:
groups = fields_to_group_by.split(",")
# sanitize input
groups = self.allowed_groups.intersection(set(groups))
# only grouped fields remain
self.fields = self.fields.intersection(groups)
# create queryset
self.queryset = Record.objects.values(*groups)
def aggregate(self, fields_to_sum: str):
if fields_to_sum:
fields_to_sum = fields_to_sum.split(",")
# sanitize input
fields_to_sum = self.allowed_fields.intersection(set(fields_to_sum))
# add aggregated fields
self.fields = self.fields.union(fields_to_sum)
annotations = self.get_annotations_dict(fields_to_sum)
self.queryset = self.queryset.annotate(**annotations)
def get_annotations_dict(self, annotation_fields: set) -> dict:
annotations = dict()
for field in annotation_fields:
if field == "cpi":
cpi_formula = Sum("spend", output_field=FloatField()) / Sum(
"installs", output_field=FloatField()
)
annotations[field] = cpi_formula
# since cpi is an optional field, only add when necessary
self.fields.add("cpi")
else:
annotations[field] = Sum(field)
return annotations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment