start adding new ninja-based OpenAPI-compatible API v2, starting with mesh

This commit is contained in:
Laura Klünder 2023-11-11 03:01:15 +01:00
parent 0b6362c8ab
commit f5c33724dc
9 changed files with 101 additions and 4 deletions

View file

@ -4,6 +4,7 @@ from collections import OrderedDict
from django.urls import include, path, re_path
from django.utils.functional import cached_property
from ninja import NinjaAPI
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from rest_framework.routers import SimpleRouter
@ -18,8 +19,16 @@ from c3nav.mapdata.api import (AccessRestrictionGroupViewSet, AccessRestrictionV
SpaceViewSet, StairViewSet, UpdatesViewSet)
from c3nav.mapdata.utils.user import can_access_editor
from c3nav.mesh.api import FirmwareViewSet
from c3nav.mesh.newapi import api_router as mesh_api_router
from c3nav.routing.api import RoutingViewSet
ninja_api = NinjaAPI(
title="c3nav API",
version="v2",
docs_url="/",
)
ninja_api.add_router("/mesh/", mesh_api_router)
router = SimpleRouter()
router.register(r'map', MapViewSet, basename='map')
router.register(r'levels', LevelViewSet)
@ -63,6 +72,8 @@ class APIRoot(GenericAPIView):
The HTML preview is only shown because your Browser sent text/html in its Accept header.
If you want to use this API on a large scale, please use a client that supports E-Tags.
For more information on a specific API endpoint, access it with a browser.
This is the old API which is slowly being phased out in favor of the new API at /api/v2/.
"""
def _format_pattern(self, pattern):

View file

@ -1,3 +1,7 @@
from enum import EnumMeta
from typing import Any, Optional, cast, Iterator, Callable
from pydantic.fields import ModelField
from rest_framework.exceptions import ParseError
@ -10,3 +14,26 @@ def get_api_post_data(request):
raise ParseError('Invalid JSON.')
return data
return request.POST
class EnumSchemaByNameMixin:
@classmethod
def __modify_schema__(cls, field_schema: dict[str, Any], field: Optional[ModelField]) -> None:
if field is None:
return
field_schema["enum"] = list(cast(EnumMeta, field.type_).__members__.keys())
@classmethod
def _validate(cls, v: Any, field: ModelField) -> Any:
if isinstance(v, cls):
# it's already the object, so it's going to json, return string
return v.name
try:
# it's a string, so it's coming from json, return object
return cls[v]
except KeyError:
raise ValueError(f"Invalid value for {cls}: `{v}`")
@classmethod
def __get_validators__(cls) -> Iterator[Callable[..., Any]]:
yield cls._validate

View file

@ -2,6 +2,7 @@ import re
from dataclasses import dataclass, field
from enum import IntEnum, unique
from c3nav.api.utils import EnumSchemaByNameMixin
from c3nav.mesh.baseformats import (BoolFormat, EnumFormat, FixedHexFormat, FixedStrFormat, SimpleFormat, StructType,
VarArrayFormat)
@ -76,7 +77,7 @@ class UWBConfig(StructType):
@unique
class BoardType(IntEnum):
class BoardType(EnumSchemaByNameMixin, IntEnum):
CUSTOM = 0x00
# devboards

View file

@ -5,6 +5,7 @@ from typing import TypeVar
import channels
from channels.db import database_sync_to_async
from c3nav.api.utils import EnumSchemaByNameMixin
from c3nav.mesh.baseformats import (BoolFormat, EnumFormat, FixedStrFormat, SimpleFormat, StructType, VarArrayFormat,
VarBytesFormat, VarStrFormat, normalize_name)
from c3nav.mesh.dataformats import (BoardConfig, FirmwareAppDescription, MacAddressesListFormat, MacAddressFormat,
@ -68,7 +69,7 @@ M = TypeVar('M', bound='MeshMessage')
@unique
class ChipType(IntEnum):
class ChipType(EnumSchemaByNameMixin, IntEnum):
ESP32_S2 = 2
ESP32_C3 = 5

View file

@ -385,7 +385,7 @@ class FirmwareBuild(models.Model):
'chip': ChipType(self.chip).name,
'sha256_hash': self.sha256_hash,
'url': self.binary.url,
'boards': self.boards,
'boards': [board.name for board in self.boards],
}
@cached_property

52
src/c3nav/mesh/newapi.py Normal file
View file

@ -0,0 +1,52 @@
from datetime import datetime
from ninja import Router as APIRouter, Field as APIField, Schema
from ninja.pagination import paginate
from c3nav.mesh.dataformats import BoardType
from c3nav.mesh.messages import ChipType
from c3nav.mesh.models import FirmwareVersion
api_router = APIRouter(tags=["mesh"])
class FirmwareBuildSchema(Schema):
id: int
chip: ChipType = APIField(..., example=ChipType.ESP32_C3.name)
sha256_hash: str = APIField(..., regex=r"^[0-9a-f]{64}$")
url: str = APIField(..., alias="binary", example="/media/firmware/012345/firmware.bin")
boards: list[BoardType] = APIField(..., example=[BoardType.C3NAV_LOCATION_PCB_REV_0_2.name, ])
@staticmethod
def resolve_chip(obj):
# todo: do this in model? idk
return ChipType(obj.chip)
class FirmwareSchema(Schema):
id: int
project_name: str = APIField(..., example="c3nav_positioning")
version: str = APIField(..., example="499837d-dirty")
idf_version: str = APIField(..., example="v5.1-476-g3187b8b326")
created: datetime
builds: list[FirmwareBuildSchema]
class Error(Schema):
detail: str
@api_router.get('/firmwares/', response=list[FirmwareSchema],
summary="List available firmwares")
@paginate
def firmware_list(request):
return FirmwareVersion.objects.all()
@api_router.get('/firmwares/{firmware_id}/', response={200: FirmwareSchema, 404: Error},
summary="Get specific firmware")
def firmware_detail(request, firmware_id: int):
try:
return FirmwareVersion.objects.get(id=firmware_id)
except FirmwareVersion.DoesNotExist:
return 404, {"detail": "firmware not found"}

View file

@ -248,6 +248,7 @@ INSTALLED_APPS = [
'channels',
'compressor',
'bootstrap3',
'ninja',
'c3nav.api',
'rest_framework',
'c3nav.mapdata',
@ -329,6 +330,8 @@ REST_FRAMEWORK = {
)
}
NINJA_PAGINATION_CLASS = "ninja.pagination.LimitOffsetPagination"
LOCALE_PATHS = (
PROJECT_DIR / 'locale',
)

View file

@ -15,6 +15,7 @@ import c3nav.site.urls
urlpatterns = [
path('editor/', include(c3nav.editor.urls)),
path('api/v2/', c3nav.api.urls.ninja_api.urls),
path('api/', include((c3nav.api.urls, 'api'), namespace='api')),
path('map/', include(c3nav.mapdata.urls)),
path('admin/', admin.site.urls),

View file

@ -3,6 +3,7 @@ django-bootstrap3==23.1
django-compressor==4.3.1
csscompressor==0.9.5
djangorestframework==3.14.0
django-ninja==0.22.2
django-filter==23.2
shapely==2.0.1
pybind11==2.10.4