some more documentation in the API and in the API code
This commit is contained in:
parent
f96e916184
commit
0f43274e33
8 changed files with 126 additions and 24 deletions
|
@ -2,11 +2,16 @@ from django.conf import settings
|
||||||
from ninja import Router as APIRouter
|
from ninja import Router as APIRouter
|
||||||
from ninja import Schema
|
from ninja import Schema
|
||||||
|
|
||||||
|
from c3nav.api.utils import NonEmptyStr
|
||||||
|
|
||||||
auth_api_router = APIRouter(tags=["auth"])
|
auth_api_router = APIRouter(tags=["auth"])
|
||||||
|
|
||||||
|
|
||||||
class APITokenSchema(Schema):
|
class APITokenSchema(Schema):
|
||||||
token: str
|
"""
|
||||||
|
An API token to be used with Bearer authentication
|
||||||
|
"""
|
||||||
|
token: NonEmptyStr
|
||||||
|
|
||||||
|
|
||||||
@auth_api_router.get('/session/', response=APITokenSchema, auth=None,
|
@auth_api_router.get('/session/', response=APITokenSchema, auth=None,
|
||||||
|
|
|
@ -14,7 +14,20 @@ from c3nav.control.models import UserPermissions
|
||||||
FakeRequest = namedtuple('FakeRequest', ('session', ))
|
FakeRequest = namedtuple('FakeRequest', ('session', ))
|
||||||
|
|
||||||
|
|
||||||
class BearerAuth(HttpBearer):
|
description = """
|
||||||
|
An API token can be acquired in 4 ways:
|
||||||
|
|
||||||
|
* Use `anonymous` for guest access.
|
||||||
|
* Generate a session-bound token using the auth session endpoint.
|
||||||
|
* Create an API token in your user account settings.
|
||||||
|
* Create an API token by signing in through the auth endpoint.
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
class APITokenAuth(HttpBearer):
|
||||||
|
openapi_name = "api token authentication"
|
||||||
|
openapi_description = description
|
||||||
|
|
||||||
def __init__(self, logged_in=False, superuser=False):
|
def __init__(self, logged_in=False, superuser=False):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.logged_in = superuser or logged_in
|
self.logged_in = superuser or logged_in
|
||||||
|
|
|
@ -3,9 +3,16 @@ from typing import Literal
|
||||||
from ninja import Schema
|
from ninja import Schema
|
||||||
from pydantic import Field as APIField
|
from pydantic import Field as APIField
|
||||||
|
|
||||||
|
from c3nav.api.utils import NonEmptyStr
|
||||||
|
|
||||||
|
|
||||||
class APIErrorSchema(Schema):
|
class APIErrorSchema(Schema):
|
||||||
detail: str
|
"""
|
||||||
|
An error has occured with this request
|
||||||
|
"""
|
||||||
|
detail: NonEmptyStr = APIField(
|
||||||
|
description="A human-readable error description"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PolygonSchema(Schema):
|
class PolygonSchema(Schema):
|
||||||
|
|
|
@ -5,7 +5,7 @@ from collections import OrderedDict
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from ninja import NinjaAPI
|
from ninja import NinjaAPI, Swagger
|
||||||
from ninja.schema import NinjaGenerateJsonSchema
|
from ninja.schema import NinjaGenerateJsonSchema
|
||||||
from rest_framework.generics import GenericAPIView
|
from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -14,7 +14,7 @@ from rest_framework.routers import SimpleRouter
|
||||||
from c3nav.api.api import SessionViewSet
|
from c3nav.api.api import SessionViewSet
|
||||||
from c3nav.api.exceptions import CustomAPIException
|
from c3nav.api.exceptions import CustomAPIException
|
||||||
from c3nav.api.newapi import auth_api_router
|
from c3nav.api.newapi import auth_api_router
|
||||||
from c3nav.api.newauth import BearerAuth
|
from c3nav.api.newauth import APITokenAuth
|
||||||
from c3nav.editor.api import ChangeSetViewSet, EditorViewSet
|
from c3nav.editor.api import ChangeSetViewSet, EditorViewSet
|
||||||
from c3nav.mapdata.api import (AccessRestrictionGroupViewSet, AccessRestrictionViewSet, AreaViewSet, BuildingViewSet,
|
from c3nav.mapdata.api import (AccessRestrictionGroupViewSet, AccessRestrictionViewSet, AreaViewSet, BuildingViewSet,
|
||||||
ColumnViewSet, CrossDescriptionViewSet, DoorViewSet, DynamicLocationPositionViewSet,
|
ColumnViewSet, CrossDescriptionViewSet, DoorViewSet, DynamicLocationPositionViewSet,
|
||||||
|
@ -28,11 +28,42 @@ from c3nav.mesh.api import FirmwareViewSet
|
||||||
from c3nav.mesh.newapi import mesh_api_router
|
from c3nav.mesh.newapi import mesh_api_router
|
||||||
from c3nav.routing.api import RoutingViewSet
|
from c3nav.routing.api import RoutingViewSet
|
||||||
|
|
||||||
|
description = """
|
||||||
|
Nearly all endpoints require authentication, but guest authentication can be used.
|
||||||
|
|
||||||
|
API endpoints may change to add more features and properties,
|
||||||
|
but no properties will be removed without a version change.
|
||||||
|
""".strip()
|
||||||
|
|
||||||
ninja_api = NinjaAPI(
|
ninja_api = NinjaAPI(
|
||||||
title="c3nav API",
|
title="c3nav API",
|
||||||
version="v2",
|
version="v2",
|
||||||
|
description=description,
|
||||||
|
|
||||||
docs_url="/",
|
docs_url="/",
|
||||||
auth=(None if settings.DEBUG else BearerAuth()),
|
docs=Swagger(settings={
|
||||||
|
"persistAuthorization": True,
|
||||||
|
"defaultModelRendering": "model",
|
||||||
|
}),
|
||||||
|
|
||||||
|
auth=APITokenAuth(),
|
||||||
|
|
||||||
|
openapi_extra={
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "auth",
|
||||||
|
"description": "Get and manage API access",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "map",
|
||||||
|
"description": "Access the map data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mesh",
|
||||||
|
"description": "Manage the location node mesh network",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,7 +72,9 @@ def on_invalid_token(request, exc):
|
||||||
return ninja_api.create_response(request, {"detail": exc.detail}, status=exc.status_code)
|
return ninja_api.create_response(request, {"detail": exc.detail}, status=exc.status_code)
|
||||||
|
|
||||||
|
|
||||||
# ugly hack: remove schema from the end of definition names
|
"""
|
||||||
|
ugly hack: remove schema from the end of definition names
|
||||||
|
"""
|
||||||
orig_normalize_name = NinjaGenerateJsonSchema.normalize_name
|
orig_normalize_name = NinjaGenerateJsonSchema.normalize_name
|
||||||
def wrap_normalize_name(self, name: str): # noqa
|
def wrap_normalize_name(self, name: str): # noqa
|
||||||
return orig_normalize_name(self, name).removesuffix('Schema')
|
return orig_normalize_name(self, name).removesuffix('Schema')
|
||||||
|
|
|
@ -77,7 +77,11 @@ class LocationSchema(AccessRestrictionSchema, TitledSchema, LocationSlugSchema,
|
||||||
# todo: add_search
|
# todo: add_search
|
||||||
|
|
||||||
|
|
||||||
class LabelSettingsSchema(TitledSchema, Schema):
|
class LabelSettingsSchema(TitledSchema, DjangoModelSchema):
|
||||||
|
"""
|
||||||
|
Settings preset for how and when to display a label. Reusable between objects.
|
||||||
|
The title describes the title of this preset, not the displayed label.
|
||||||
|
"""
|
||||||
min_zoom: float = APIField(
|
min_zoom: float = APIField(
|
||||||
title="min zoom",
|
title="min zoom",
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ninja import Schema
|
from ninja import Schema, Swagger
|
||||||
from pydantic import Field as APIField
|
from pydantic import Field as APIField
|
||||||
from pydantic import PositiveFloat, PositiveInt
|
from pydantic import PositiveFloat, PositiveInt
|
||||||
|
|
||||||
|
@ -9,6 +9,11 @@ from c3nav.mapdata.schemas.model_base import SpecificLocationSchema
|
||||||
|
|
||||||
|
|
||||||
class LevelSchema(SpecificLocationSchema):
|
class LevelSchema(SpecificLocationSchema):
|
||||||
|
"""
|
||||||
|
A physical level of the map, containing building, spaces, doors…
|
||||||
|
|
||||||
|
A level is a specific location, and can therefor be routed to and from, as well as belong to location groups.
|
||||||
|
"""
|
||||||
short_label: NonEmptyStr = APIField(
|
short_label: NonEmptyStr = APIField(
|
||||||
title="short label (for level selector)",
|
title="short label (for level selector)",
|
||||||
description="unique among levels",
|
description="unique among levels",
|
||||||
|
|
|
@ -3,4 +3,10 @@ from pydantic import Field as APIField
|
||||||
|
|
||||||
|
|
||||||
class BoundsSchema(Schema):
|
class BoundsSchema(Schema):
|
||||||
bounds: tuple[tuple[float, float], tuple[float, float]] = APIField(..., example=((-10, -20), (20, 30)))
|
"""
|
||||||
|
Describing a bounding box
|
||||||
|
"""
|
||||||
|
bounds: tuple[tuple[float, float], tuple[float, float]] = APIField(
|
||||||
|
description="(x, y) to (x, y)",
|
||||||
|
example=((-10, -20), (20, 30)),
|
||||||
|
)
|
||||||
|
|
|
@ -6,10 +6,11 @@ from ninja import Field as APIField
|
||||||
from ninja import Router as APIRouter
|
from ninja import Router as APIRouter
|
||||||
from ninja import Schema, UploadedFile
|
from ninja import Schema, UploadedFile
|
||||||
from ninja.pagination import paginate
|
from ninja.pagination import paginate
|
||||||
|
from ninja.renderers import BaseRenderer
|
||||||
from pydantic import PositiveInt, field_validator
|
from pydantic import PositiveInt, field_validator
|
||||||
|
|
||||||
from c3nav.api.exceptions import API404, APIConflict, APIRequestValidationFailed
|
from c3nav.api.exceptions import API404, APIConflict, APIRequestValidationFailed
|
||||||
from c3nav.api.newauth import BearerAuth, auth_permission_responses, auth_responses, validate_responses
|
from c3nav.api.newauth import APITokenAuth, auth_permission_responses, auth_responses, validate_responses
|
||||||
from c3nav.mesh.dataformats import BoardType, ChipType, FirmwareImage
|
from c3nav.mesh.dataformats import BoardType, ChipType, FirmwareImage
|
||||||
from c3nav.mesh.models import FirmwareBuild, FirmwareVersion
|
from c3nav.mesh.models import FirmwareBuild, FirmwareVersion
|
||||||
|
|
||||||
|
@ -17,25 +18,47 @@ mesh_api_router = APIRouter(tags=["mesh"])
|
||||||
|
|
||||||
|
|
||||||
class FirmwareBuildSchema(Schema):
|
class FirmwareBuildSchema(Schema):
|
||||||
|
"""
|
||||||
|
A build belonging to a firmware version.
|
||||||
|
"""
|
||||||
id: PositiveInt
|
id: PositiveInt
|
||||||
variant: str = APIField(..., example="c3uart")
|
variant: str = APIField(
|
||||||
chip: ChipType = APIField(..., example=ChipType.ESP32_C3.name)
|
description="a variant identifier for this build, unique for this firmware version",
|
||||||
sha256_hash: str = APIField(..., pattern=r"^[0-9a-f]{64}$")
|
example="c3uart"
|
||||||
url: str = APIField(..., alias="binary", example="/media/firmware/012345/firmware.bin") # todo: downlaod differently?
|
)
|
||||||
|
chip: ChipType = APIField(
|
||||||
|
description="the chip that this build was built for",
|
||||||
|
example=ChipType.ESP32_C3.name,
|
||||||
|
)
|
||||||
|
sha256_hash: str = APIField(
|
||||||
|
description="SHE256 hash of the underlying ELF file",
|
||||||
|
pattern=r"^[0-9a-f]{64}$",
|
||||||
|
)
|
||||||
|
url: str = APIField(
|
||||||
|
alias="binary",
|
||||||
|
example="/media/firmware/012345/firmware.bin",
|
||||||
|
description="download URL for the build binary",
|
||||||
|
) # todo: downlaod differently?
|
||||||
# todo: should not be none, but parse errors
|
# todo: should not be none, but parse errors
|
||||||
boards: list[BoardType] = APIField(None, example=[BoardType.C3NAV_LOCATION_PCB_REV_0_2.name, ])
|
boards: set[BoardType] = APIField(
|
||||||
|
description="set of boards that this build is compatible with",
|
||||||
|
example={BoardType.C3NAV_LOCATION_PCB_REV_0_2.name, }
|
||||||
|
)
|
||||||
|
|
||||||
class Config(Schema.Config):
|
class Config(Schema.Config):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FirmwareSchema(Schema):
|
class FirmwareSchema(Schema):
|
||||||
|
"""
|
||||||
|
A firmware version, usually with multiple build variants.
|
||||||
|
"""
|
||||||
id: PositiveInt
|
id: PositiveInt
|
||||||
project_name: str = APIField(..., example="c3nav_positioning")
|
project_name: str = APIField(..., example="c3nav_positioning")
|
||||||
version: str = APIField(..., example="499837d-dirty")
|
version: str = APIField(..., example="499837d-dirty")
|
||||||
idf_version: str = APIField(..., example="v5.1-476-g3187b8b326")
|
idf_version: str = APIField(..., example="v5.1-476-g3187b8b326")
|
||||||
created: datetime
|
created: datetime
|
||||||
builds: list[FirmwareBuildSchema]
|
builds: list[FirmwareBuildSchema] = APIField(min_items=1)
|
||||||
|
|
||||||
@field_validator('builds')
|
@field_validator('builds')
|
||||||
def builds_variants_must_be_unique(cls, builds):
|
def builds_variants_must_be_unique(cls, builds):
|
||||||
|
@ -44,10 +67,6 @@ class FirmwareSchema(Schema):
|
||||||
return builds
|
return builds
|
||||||
|
|
||||||
|
|
||||||
class Error(Schema):
|
|
||||||
detail: str
|
|
||||||
|
|
||||||
|
|
||||||
@mesh_api_router.get('/firmwares/', summary="List available firmwares",
|
@mesh_api_router.get('/firmwares/', summary="List available firmwares",
|
||||||
response={200: list[FirmwareSchema], **validate_responses, **auth_responses})
|
response={200: list[FirmwareSchema], **validate_responses, **auth_responses})
|
||||||
@paginate
|
@paginate
|
||||||
|
@ -66,7 +85,9 @@ def firmware_detail(request, firmware_id: int):
|
||||||
|
|
||||||
@mesh_api_router.get('/firmwares/{firmware_id}/{variant}/image_data',
|
@mesh_api_router.get('/firmwares/{firmware_id}/{variant}/image_data',
|
||||||
summary="Get header data of firmware build image",
|
summary="Get header data of firmware build image",
|
||||||
response={200: FirmwareImage.schema, **API404.dict(), **auth_responses})
|
response={200: FirmwareImage.schema, **API404.dict(), **auth_responses},
|
||||||
|
openapi_extra={"externalDocs": "https://docs.espressif.com/projects/esptool/en/latest/esp32s3/"
|
||||||
|
"advanced-topics/firmware-image-format.html"})
|
||||||
def firmware_build_image(request, firmware_id: int, variant: str):
|
def firmware_build_image(request, firmware_id: int, variant: str):
|
||||||
try:
|
try:
|
||||||
build = FirmwareBuild.objects.get(version_id=firmware_id, variant=variant)
|
build = FirmwareBuild.objects.get(version_id=firmware_id, variant=variant)
|
||||||
|
@ -77,7 +98,9 @@ def firmware_build_image(request, firmware_id: int, variant: str):
|
||||||
|
|
||||||
@mesh_api_router.get('/firmwares/{firmware_id}/{variant}/project_description',
|
@mesh_api_router.get('/firmwares/{firmware_id}/{variant}/project_description',
|
||||||
summary="Get project description of firmware build",
|
summary="Get project description of firmware build",
|
||||||
response={200: dict, **API404.dict(), **auth_responses})
|
response={200: dict, **API404.dict(), **auth_responses},
|
||||||
|
openapi_extra={"externalDocs": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32/"
|
||||||
|
"api-guides/build-system.html#build-system-metadata"})
|
||||||
def firmware_project_description(request, firmware_id: int, variant: str):
|
def firmware_project_description(request, firmware_id: int, variant: str):
|
||||||
try:
|
try:
|
||||||
return FirmwareBuild.objects.get(version_id=firmware_id, variant=variant).firmware_description
|
return FirmwareBuild.objects.get(version_id=firmware_id, variant=variant).firmware_description
|
||||||
|
@ -86,6 +109,9 @@ def firmware_project_description(request, firmware_id: int, variant: str):
|
||||||
|
|
||||||
|
|
||||||
class UploadFirmwareBuildSchema(Schema):
|
class UploadFirmwareBuildSchema(Schema):
|
||||||
|
"""
|
||||||
|
A firmware build to upload, with at least one build variant
|
||||||
|
"""
|
||||||
variant: str = APIField(..., example="c3uart")
|
variant: str = APIField(..., example="c3uart")
|
||||||
boards: list[BoardType] = APIField(..., example=[BoardType.C3NAV_LOCATION_PCB_REV_0_2.name, ])
|
boards: list[BoardType] = APIField(..., example=[BoardType.C3NAV_LOCATION_PCB_REV_0_2.name, ])
|
||||||
project_description: dict = APIField(..., title='project_description.json contents')
|
project_description: dict = APIField(..., title='project_description.json contents')
|
||||||
|
@ -93,10 +119,13 @@ class UploadFirmwareBuildSchema(Schema):
|
||||||
|
|
||||||
|
|
||||||
class UploadFirmwareSchema(Schema):
|
class UploadFirmwareSchema(Schema):
|
||||||
|
"""
|
||||||
|
A firmware version to upload, with at least one build variant
|
||||||
|
"""
|
||||||
project_name: str = APIField(..., example="c3nav_positioning")
|
project_name: str = APIField(..., example="c3nav_positioning")
|
||||||
version: str = APIField(..., example="499837d-dirty")
|
version: str = APIField(..., example="499837d-dirty")
|
||||||
idf_version: str = APIField(..., example="v5.1-476-g3187b8b326")
|
idf_version: str = APIField(..., example="v5.1-476-g3187b8b326")
|
||||||
builds: list[UploadFirmwareBuildSchema] = APIField(..., min_items=1)
|
builds: list[UploadFirmwareBuildSchema] = APIField(min_items=1)
|
||||||
|
|
||||||
@field_validator('builds')
|
@field_validator('builds')
|
||||||
def builds_variants_must_be_unique(cls, builds):
|
def builds_variants_must_be_unique(cls, builds):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue