replace DRF with django-ninja

This commit is contained in:
Jan Koppe 2024-03-17 21:02:41 +01:00
parent 4299d46b7b
commit 813780a18b
Signed by: thunfisch
GPG Key ID: BE935B0735A2129B
15 changed files with 296 additions and 183 deletions

112
source/concierge/api.py Normal file
View File

@ -0,0 +1,112 @@
from ninja import Router, Schema
from ninja.errors import HttpError
from concierge import models
from typing import List
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils.timezone import now
from django.views.decorators.csrf import csrf_exempt
import json
router = Router()
class ClaimReference(Schema):
uuid: str
class AvailableTask(Schema):
uuid: str
type: str
class HeartbeatResponse(Schema):
success: bool = True
claims: List[ClaimReference]
available: List[AvailableTask]
class ClaimResponse(Schema):
uuid: str
type: str
configuration: dict
class ReleaseResponse(Schema):
success: bool = True
uuid: str
type: str
@router.post('/heartbeat/{identity}', response=HeartbeatResponse)
@csrf_exempt
def receive_heartbeat(request, identity: str):
try:
id = models.Identity.objects.get(identity=identity)
except models.Identity.DoesNotExist:
raise HttpError(403, "identity unknown")
# update heartbeat
id.heartbeat = now()
id.save()
# get current claims and available tasks
claims = models.Task.objects.filter(claimed_by=id).all()
available = models.Task.objects.filter(claimed_by=None).all()
return {
'success': True,
'claims': [{'uuid': str(o.uuid)} for o in list(claims)],
'available': [{'uuid': str(o.uuid), 'type': o.type} for o in list(available)],
}
@router.post('/claim/{identity}/{task_uuid}', response=ClaimResponse)
@csrf_exempt
def claim_task(request, identity: str, task_uuid: str):
try:
id = models.Identity.objects.get(identity=identity)
except models.Identity.DoesNotExist:
raise HttpError(403, "identity unknown")
with transaction.atomic():
task = get_object_or_404(models.Task, uuid=task_uuid)
if task.claimed_by:
raise HttpError(423, "task already claimed")
task.claimed_by = id
task.save()
return {
'success': True,
'uuid': task.uuid,
'type': task.type,
'configuration': json.loads(task.configuration)
}
@router.post('/release/{identity}/{task_uuid}')
@csrf_exempt
def release_task(request, identity: str, task_uuid: str):
try:
id = models.Identity.objects.get(identity=identity)
except models.Identity.DoesNotExist:
raise HttpError(403, "identity unknown")
with transaction.atomic():
task = get_object_or_404(models.Task, uuid=task_uuid)
if task.claimed_by != id:
raise HttpError(403, "task not claimed by this identity")
task.claimed_by = None
task.save()
return {
'success': True,
'uuid': task.uuid,
'type': task.type,
}

View File

@ -1,8 +0,0 @@
from django.urls import path
from . import views
urlpatterns = [
path('api/<uuid:identity>/heartbeat', views.heartbeat, name='heartbeat'),
path('api/<uuid:identity>/claim/<uuid:task_uuid>', views.claim, name='claim'),
path('api/<uuid:identity>/release/<uuid:task_uuid>', views.release, name='release'),
]

View File

@ -1,94 +0,0 @@
import json
from django.db import transaction
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.core.exceptions import ObjectDoesNotExist
from django.utils.timezone import now
from .models import Identity, Task
@csrf_exempt
@require_POST
def heartbeat(request, identity):
try:
id = Identity.objects.get(identity=identity)
except ObjectDoesNotExist:
return JsonResponse({'error': 'identity unknown'}, status=403)
# update heartbeat
id.heartbeat = now()
id.save()
# get current claims and available tasks
claims = Task.objects.filter(claimed_by=id).all()
available = Task.objects.filter(claimed_by=None).all()
data = {
'success': True,
'claims': [{'uuid': str(o.uuid)} for o in list(claims)],
'available': [{'uuid': str(o.uuid), 'type': o.type} for o in list(available)],
}
return JsonResponse(data)
@csrf_exempt
@require_POST
def claim(request, identity, task_uuid):
try:
id = Identity.objects.get(identity=identity)
except ObjectDoesNotExist:
return JsonResponse({'error': 'identity unknown'}, status=403)
with transaction.atomic():
try:
task = Task.objects.get(uuid=task_uuid)
except ObjectDoesNotExist:
return JsonResponse({'error': 'task unknown'}, status=404)
if task.claimed_by:
return JsonResponse({'error': 'task already claimed'}, status=423)
task.claimed_by = id
task.save()
data = {
'success': True,
'uuid': task.uuid,
'type': task.type,
'configuration': json.loads(task.configuration)
}
return JsonResponse(data)
@csrf_exempt
@require_POST
def release(request, identity, task_uuid):
try:
id = Identity.objects.get(identity=identity)
except ObjectDoesNotExist:
return JsonResponse({'error': 'identity unknown'}, status=403)
with transaction.atomic():
try:
task = Task.objects.get(uuid=task_uuid)
except ObjectDoesNotExist:
return JsonResponse({'error': 'task unknown'}, status=404)
if task.claimed_by != id:
return JsonResponse({'error': 'task claimed by other identity'}, status=403)
task.claimed_by = None
task.save()
data = {
'success': True,
'uuid': task.uuid,
'type': task.type,
}
return JsonResponse(data)

144
source/config/api.py Normal file
View File

@ -0,0 +1,144 @@
from ninja import Router, ModelSchema, Schema
from ninja.errors import HttpError
from config import models
from typing import List
from guardian.shortcuts import get_objects_for_user, assign_perm
from django.shortcuts import get_object_or_404
router = Router()
class Restream(ModelSchema):
class Meta:
model = models.Restream
fields = "__all__"
class RestreamPatch(ModelSchema):
class Meta:
model = models.Restream
exclude = ["id"]
fields_optional = "__all__"
extra = "forbid"
class Stream(ModelSchema):
class Meta:
model = models.Stream
fields = "__all__"
class StreamPatch(ModelSchema):
class Meta:
model = models.Stream
fields = ["name"]
fields_optional = "__all__"
extra = "forbid"
class StreamCreate(ModelSchema):
class Meta:
model = models.Stream
fields = ["name"]
extra = "forbid"
@router.get('/streams', response=List[Stream])
def list_streams(request):
return get_objects_for_user(request.user, 'view_stream', models.Stream.objects.all())
@router.post('/streams', response=Stream)
def create_stream(request, payload: StreamCreate):
stream = models.Stream.objects.create(**payload.dict())
assign_perm('view_stream', request.user, stream)
assign_perm('change_stream', request.user, stream)
assign_perm('delete_stream', request.user, stream)
return stream
@router.get('/streams/{id}', response=Stream)
def get_stream(request, id: int):
stream = get_object_or_404(models.Stream, id=id)
if not request.user.has_perm('view_stream', stream):
raise HttpError(401, "unauthorized")
return stream
@router.patch('/streams/{id}', response=Stream)
def update_stream(request, id: int, payload: StreamPatch):
stream = get_object_or_404(models.Stream, id=id)
if not request.user.has_perm('change_stream', stream):
raise HttpError(401, "unauthorized")
stream.name = payload.name
stream.save()
return stream
@router.delete('/streams/{id}', response=None)
def delete_stream(request, id: int):
stream = get_object_or_404(models.Stream, id=id)
if not request.user.has_perm('delete_stream', stream):
raise HttpError(401, "unauthorized")
stream.delete()
@router.get('/restreams', response=List[Restream])
def list_restreams(request):
return get_objects_for_user(request.user, 'view_restream', models.Restream.objects.all())
@router.post('/restreams', response=Restream)
def create_restream(request, payload: Restream):
if not request.user.has_perm('view_stream', payload.stream):
raise HttpError(401, "unauthorized")
restream = models.Restream.objects.create(**payload.dict())
assign_perm('view_restream', request.user, restream)
assign_perm('change_restream', request.user, restream)
assign_perm('delete_restream', request.user, restream)
return restream
@router.get('/restreams/{id}', response=Restream)
def get_restream(request, id: int):
restream = get_object_or_404(models.Restream, id=id)
if not request.user.has_perm('view_restream', restream):
raise HttpError(401, "unauthorized")
return restream
@router.patch('/restreams/{id}', response=Restream)
def update_restream(request, id: int, payload: RestreamPatch):
restream = get_object_or_404(models.Restream, id=id)
if not request.user.has_perm('change_restream', restream):
raise HttpError(401, "unauthorized")
if payload.stream:
payload.stream = get_object_or_404(models.Stream, id=payload.stream)
if not request.user.has_perm('view_stream', payload.stream):
raise HttpError(401, "unauthorized")
for key, value in payload.dict(exclude_unset=True).items():
setattr(restream, key, value)
restream.save()
return restream
@router.delete('/restreams/{id}', response=None)
def delete_restream(request, id: int):
restream = get_object_or_404(models.Restream, id=id)
if not request.user.has_perm('delete_restream', restream):
raise HttpError(401, "unauthorized")
restream.delete()

30
source/portier/api.py Normal file
View File

@ -0,0 +1,30 @@
from ninja import NinjaAPI, ModelSchema, Router
from ninja.security import django_auth
from django.contrib.auth.models import User
from config.api import router as config_router
from concierge.api import router as concierge_router
core_router = Router()
class UserSchema(ModelSchema):
class Meta:
model = User
fields = ["id", "username", "email"]
@core_router.get("/me", response=UserSchema)
def me(request):
return request.user
api = NinjaAPI(
title="Portier API",
version="2.0.0",
description="HTTP API for Portier. Use this to interact with the Portier backend.",
csrf=True,
)
api.add_router("/", core_router, auth=django_auth, tags=["Core"])
api.add_router("/config/", config_router, auth=django_auth, tags=["Configuration"])
api.add_router("/concierge/", concierge_router, auth=None, tags=["Concierge"])

View File

@ -41,7 +41,8 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'rest_framework', 'ninja',
#'rest_framework',
'guardian', 'guardian',
'django_registration', 'django_registration',
'bootstrap4', 'bootstrap4',
@ -194,7 +195,7 @@ DJANGO_CELERY_BEAT_TZ_AWARE = False
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
if DEBUG: if DEBUG == "LOLOLOL":
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,

View File

@ -1,13 +1,14 @@
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from .api import api
urlpatterns = [ urlpatterns = [
path('accounts/', include('django_registration.backends.activation.urls')), path('accounts/', include('django_registration.backends.activation.urls')),
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),
path('i18n/', include('django.conf.urls.i18n')), path('i18n/', include('django.conf.urls.i18n')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('config/', include('config.urls')), path('config/', include('config.urls')),
path('concierge/', include('concierge.urls')), path('api/v2/', api.urls),
path('api/v1/', include('restapi.urls')),
path('', include('core.urls')), path('', include('core.urls')),
] ]

View File

@ -5,8 +5,7 @@ django-guardian
django-fontawesome-5 django-fontawesome-5
django-celery-beat django-celery-beat
django-filter django-filter
djangorestframework django-ninja
djangorestframework-guardian
celery>=5.3 celery>=5.3
gunicorn>=20 gunicorn>=20
psycopg2-binary psycopg2-binary

View File

@ -1,5 +0,0 @@
from django.apps import AppConfig
class RestapiConfig(AppConfig):
name = 'restapi'

View File

@ -1,16 +0,0 @@
from django.urls import path, include
from rest_framework import routers
from .views import StreamViewSet, RestreamViewSet
router = routers.DefaultRouter()
router.register(r'stream', StreamViewSet)
router.register(r'restream', RestreamViewSet)
app_name = 'restapi'
urlpatterns = [
path('', include(router.urls)),
path('auth/', include('rest_framework.urls', namespace='rest_framework'))
]

View File

@ -1,51 +0,0 @@
from rest_framework_guardian.serializers import ObjectPermissionsAssignmentMixin
from rest_framework import serializers, viewsets
from rest_framework_guardian import filters
from config.models import Stream, Restream
class StreamSerializer(ObjectPermissionsAssignmentMixin, serializers.ModelSerializer):
class Meta:
model = Stream
fields = '__all__'
read_only_fields = ['publish_counter']
def get_permissions_map(self, created):
current_user = self.context['request'].user
return {
'view_stream': [current_user],
'change_stream': [current_user],
'delete_stream': [current_user]
}
class StreamViewSet(viewsets.ModelViewSet):
queryset = Stream.objects.all()
serializer_class = StreamSerializer
filter_backends = [filters.ObjectPermissionsFilter]
class RestreamSerializer(ObjectPermissionsAssignmentMixin, serializers.ModelSerializer):
class Meta:
model = Restream
fields = '__all__'
def get_permissions_map(self, created):
current_user = self.context['request'].user
return {
'view_restream': [current_user],
'change_restream': [current_user],
'delete_restream': [current_user]
}
def validate_stream(self, value):
request = self.context['request']
if not request.user.has_perm('config.view_stream', value):
raise serializers.ValidationError('Access to stream is not authorized')
return value
class RestreamViewSet(viewsets.ModelViewSet):
queryset = Restream.objects.all()
serializer_class = RestreamSerializer
filter_backends = [filters.ObjectPermissionsFilter]

View File

@ -13,7 +13,7 @@ var app = new Vue({
}, },
toggleActive(cfg) { toggleActive(cfg) {
axios axios
.patch('/api/v1/restream/' + cfg.id + '/', { active: !cfg.active }) .patch('/api/v2/config/restreams/' + cfg.id, { active: !cfg.active })
.then(response => { .then(response => {
i = this.cfgs.findIndex((obj => obj.id == cfg.id)) i = this.cfgs.findIndex((obj => obj.id == cfg.id))
Vue.set(this.cfgs, i, response.data) Vue.set(this.cfgs, i, response.data)
@ -22,7 +22,7 @@ var app = new Vue({
}, },
fetchData() { fetchData() {
axios axios
.get('/api/v1/restream/') .get('/api/v2/config/restreams')
.then(response => { .then(response => {
this.cfgs = response.data this.cfgs = response.data
this.isLoading = false this.isLoading = false

View File

@ -16,7 +16,7 @@ var app = new Vue({
}, },
fetchData() { fetchData() {
axios axios
.get('/api/v1/stream/') .get('/api/v2/config/streams')
.then(response => { .then(response => {
this.streams = response.data this.streams = response.data
this.isLoading = false this.isLoading = false