From 4b1ac9f19423a40f7983be6ef3f3099837b6232e Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Mon, 11 Dec 2023 20:49:50 +0100 Subject: [PATCH] convert django lazy string proxies to strings in the base schema validator, rather than in the serialization code of each source model --- src/c3nav/api/api.py | 6 ++-- src/c3nav/api/exceptions.py | 7 +++- src/c3nav/api/schema.py | 45 +++++++++++++++++++++---- src/c3nav/editor/api/endpoints.py | 2 +- src/c3nav/editor/api/schemas.py | 11 +++--- src/c3nav/editor/views/base.py | 2 +- src/c3nav/mapdata/api/map.py | 8 ++--- src/c3nav/mapdata/api/updates.py | 6 ++-- src/c3nav/mapdata/models/base.py | 2 +- src/c3nav/mapdata/models/locations.py | 12 +++---- src/c3nav/mapdata/schemas/filters.py | 4 +-- src/c3nav/mapdata/schemas/model_base.py | 40 ++++++++-------------- src/c3nav/mapdata/schemas/models.py | 20 +++++------ src/c3nav/mapdata/schemas/responses.py | 7 ++-- src/c3nav/mapdata/utils/locations.py | 2 +- src/c3nav/mapdata/utils/user.py | 4 +-- src/c3nav/mesh/api.py | 13 +++---- src/c3nav/routing/api/positioning.py | 6 ++-- src/c3nav/routing/api/routing.py | 25 +++++++------- src/c3nav/routing/route.py | 4 +-- src/c3nav/routing/schemas.py | 4 +-- 21 files changed, 126 insertions(+), 104 deletions(-) diff --git a/src/c3nav/api/api.py b/src/c3nav/api/api.py index 7d8c3e0f..354651c6 100644 --- a/src/c3nav/api/api.py +++ b/src/c3nav/api/api.py @@ -1,16 +1,16 @@ from django.conf import settings from ninja import Field as APIField from ninja import Router as APIRouter -from ninja import Schema from c3nav.api.auth import APIKeyType, auth_responses +from c3nav.api.schema import BaseSchema from c3nav.api.utils import NonEmptyStr from c3nav.control.models import UserPermissions auth_api_router = APIRouter(tags=["auth"]) -class AuthStatusSchema(Schema): +class AuthStatusSchema(BaseSchema): """ Current auth state and permissions """ @@ -44,7 +44,7 @@ def get_status(request): ) -class APIKeySchema(Schema): +class APIKeySchema(BaseSchema): key: NonEmptyStr = APIField( title="API key", description="API secret to be directly used with `X-API-Key` HTTP header." diff --git a/src/c3nav/api/exceptions.py b/src/c3nav/api/exceptions.py index b95bdf0c..7566f43f 100644 --- a/src/c3nav/api/exceptions.py +++ b/src/c3nav/api/exceptions.py @@ -1,3 +1,5 @@ +from django.utils.functional import Promise + from c3nav.api.schema import APIErrorSchema @@ -7,7 +9,10 @@ class CustomAPIException(Exception): def __init__(self, detail=None): if detail is not None: - self.detail = detail + if isinstance(detail, Promise): + self.detail = str(detail) + else: + self.detail = detail def get_response(self, api, request): return api.create_response(request, {"detail": self.detail}, status=self.status_code) diff --git a/src/c3nav/api/schema.py b/src/c3nav/api/schema.py index 649c78c8..21075f9f 100644 --- a/src/c3nav/api/schema.py +++ b/src/c3nav/api/schema.py @@ -1,13 +1,44 @@ +from types import NoneType from typing import Annotated, Any, Literal, Union +from django.utils.functional import Promise from ninja import Schema -from pydantic import Discriminator +from pydantic import Discriminator, model_validator from pydantic import Field as APIField +from pydantic.functional_validators import ModelWrapValidatorHandler +from pydantic_core.core_schema import ValidationInfo from c3nav.api.utils import NonEmptyStr -class APIErrorSchema(Schema): +class BaseSchema(Schema): + @model_validator(mode="wrap") # noqa + @classmethod + def _run_root_validator(cls, values: Any, handler: ModelWrapValidatorHandler[Schema], info: ValidationInfo) -> Any: + """ overwriting this, we need to call serialize to get the correct data """ + return handler(cls.convert(values)) + + @classmethod + def convert(cls, values: Any): + if isinstance(values, Schema): + return values + if isinstance(values, (str, bool, int, float, complex, NoneType)): + return values + if isinstance(values, dict): + return { + key: cls.convert(val) + for key, val in values.items() + } + if isinstance(values, (list, tuple, set, frozenset)): + return type(values)(cls.convert(val) for val in values) + if isinstance(values, Promise): + return str(values) + if hasattr(values, 'serialize') and callable(values.serialize): + return values.serialize() + return values + + +class APIErrorSchema(BaseSchema): """ An error has occured with this request """ @@ -16,7 +47,7 @@ class APIErrorSchema(Schema): ) -class PolygonSchema(Schema): +class PolygonSchema(BaseSchema): """ A GeoJSON Polygon """ @@ -29,7 +60,7 @@ class PolygonSchema(Schema): title = "GeoJSON Polygon" -class LineStringSchema(Schema): +class LineStringSchema(BaseSchema): """ A GeoJSON LineString """ @@ -42,7 +73,7 @@ class LineStringSchema(Schema): title = "GeoJSON LineString" -class LineSchema(Schema): +class LineSchema(BaseSchema): """ A GeoJSON LineString with only two points """ @@ -55,7 +86,7 @@ class LineSchema(Schema): title = "GeoJSON LineString (only two points)" -class PointSchema(Schema): +class PointSchema(BaseSchema): """ A GeoJSON Point """ @@ -77,7 +108,7 @@ GeometrySchema = Annotated[ ] -class AnyGeometrySchema(Schema): +class AnyGeometrySchema(BaseSchema): """ A GeoJSON Geometry """ diff --git a/src/c3nav/editor/api/endpoints.py b/src/c3nav/editor/api/endpoints.py index 78493592..00e86091 100644 --- a/src/c3nav/editor/api/endpoints.py +++ b/src/c3nav/editor/api/endpoints.py @@ -125,7 +125,7 @@ def get_view_as_api(request, path: str): resolved = resolve_editor_path_api(request, path) if not resolved: - raise API404(str(_('No matching editor view endpoint found.'))) + raise API404(_('No matching editor view endpoint found.')) if not getattr(resolved.func, 'api_hybrid', False): raise API404(_('Matching editor view point does not provide an API.')) diff --git a/src/c3nav/editor/api/schemas.py b/src/c3nav/editor/api/schemas.py index b55e90ba..6e231ed0 100644 --- a/src/c3nav/editor/api/schemas.py +++ b/src/c3nav/editor/api/schemas.py @@ -1,10 +1,9 @@ from typing import Annotated, Literal, Optional, Union -from ninja import Schema from pydantic import Field as APIField from pydantic import PositiveInt -from c3nav.api.schema import AnyGeometrySchema, GeometrySchema, LineSchema +from c3nav.api.schema import AnyGeometrySchema, GeometrySchema, LineSchema, BaseSchema from c3nav.api.utils import NonEmptyStr GeometryStylesSchema = Annotated[ @@ -47,7 +46,7 @@ EditorGeometriesCacheReferenceElem = Annotated[ ] -class EditorGeometriesPropertiesSchema(Schema): +class EditorGeometriesPropertiesSchema(BaseSchema): id: EditorID type: NonEmptyStr space: Union[ @@ -63,20 +62,20 @@ class EditorGeometriesPropertiesSchema(Schema): opacity: Optional[float] = None # todo: range -class EditorGeometriesGraphEdgePropertiesSchema(Schema): +class EditorGeometriesGraphEdgePropertiesSchema(BaseSchema): id: EditorID type: Literal["graphedge"] from_node: EditorID to_node: EditorID -class EditorGeometriesGraphEdgeElemSchema(Schema): +class EditorGeometriesGraphEdgeElemSchema(BaseSchema): type: Literal["Feature"] properties: EditorGeometriesGraphEdgePropertiesSchema geometry: LineSchema -class EditorGeometriesGeometryElemSchema(Schema): +class EditorGeometriesGeometryElemSchema(BaseSchema): type: Literal["Feature"] geometry: AnyGeometrySchema = APIField(description="geometry, potentially modified for displaying") original_geometry: Optional[GeometrySchema] = APIField( diff --git a/src/c3nav/editor/views/base.py b/src/c3nav/editor/views/base.py index 6c447764..0ba12001 100644 --- a/src/c3nav/editor/views/base.py +++ b/src/c3nav/editor/views/base.py @@ -187,7 +187,7 @@ class APIHybridFormTemplateResponse(APIHybridResponse): def get_api_response(self, request): result = {} if self.error: - result['error'] = str(self.error.message) + result['error'] = self.error.message self.status_code = self.error.status_code if request.method == 'POST': if not self.form.is_valid(): diff --git a/src/c3nav/mapdata/api/map.py b/src/c3nav/mapdata/api/map.py index b5a29b7f..41f13743 100644 --- a/src/c3nav/mapdata/api/map.py +++ b/src/c3nav/mapdata/api/map.py @@ -6,12 +6,12 @@ from django.shortcuts import redirect from django.utils import timezone from ninja import Query from ninja import Router as APIRouter -from ninja import Schema from pydantic import Field as APIField from pydantic import PositiveInt from c3nav.api.auth import auth_permission_responses, auth_responses, validate_responses from c3nav.api.exceptions import API404, APIPermissionDenied, APIRequestValidationFailed +from c3nav.api.schema import BaseSchema from c3nav.api.utils import NonEmptyStr from c3nav.mapdata.api.base import api_etag, api_stats, can_access_geometry from c3nav.mapdata.models import Source @@ -39,7 +39,7 @@ def bounds(request): } -class LocationEndpointParameters(Schema): +class LocationEndpointParameters(BaseSchema): searchable: bool = APIField( False, title='only list searchable locations', @@ -133,7 +133,7 @@ def _location_geometry(request, location): ) -class ShowRedirects(Schema): +class ShowRedirects(BaseSchema): show_redirects: bool = APIField( False, name="show redirects", @@ -268,7 +268,7 @@ def get_position_by_id(request, position_id: AnyPositionID): return location.serialize_position() -class UpdatePositionSchema(Schema): +class UpdatePositionSchema(BaseSchema): coordinates_id: Union[ Annotated[CustomLocationID, APIField(title="set coordinates")], Annotated[None, APIField(title="unset coordinates")], diff --git a/src/c3nav/mapdata/api/updates.py b/src/c3nav/mapdata/api/updates.py index 54f578f4..e746f257 100644 --- a/src/c3nav/mapdata/api/updates.py +++ b/src/c3nav/mapdata/api/updates.py @@ -4,10 +4,10 @@ from urllib.parse import urlparse from django.http import HttpResponse from ninja import Field as APIField from ninja import Router as APIRouter -from ninja import Schema from pydantic import PositiveInt from c3nav.api.auth import auth_responses +from c3nav.api.schema import BaseSchema from c3nav.api.utils import NonEmptyStr from c3nav.mapdata.models import MapUpdate from c3nav.mapdata.utils.cache.stats import increment_cache_key @@ -17,7 +17,7 @@ from c3nav.mapdata.views import set_tile_access_cookie updates_api_router = APIRouter(tags=["updates"]) -class UserDataSchema(Schema): +class UserDataSchema(BaseSchema): logged_in: bool = APIField( title="logged in", description="whether a user is logged in", @@ -52,7 +52,7 @@ class UserDataSchema(Schema): ) -class FetchUpdatesResponseSchema(Schema): +class FetchUpdatesResponseSchema(BaseSchema): last_site_update: Optional[PositiveInt] = APIField( title="ID of the last site update", description="If this ID increments, it means a major code change may have occurred. " diff --git a/src/c3nav/mapdata/models/base.py b/src/c3nav/mapdata/models/base.py index 24764dc2..66b5ecac 100644 --- a/src/c3nav/mapdata/models/base.py +++ b/src/c3nav/mapdata/models/base.py @@ -74,7 +74,7 @@ class TitledMixin(SerializableMixin, models.Model): result = super()._serialize(detailed=detailed, **kwargs) if detailed: result['titles'] = self.titles - result['title'] = str(self.title) + result['title'] = self.title return result @property diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index 2ac237de..bdc6ed99 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -88,7 +88,7 @@ class LocationSlug(SerializableMixin, models.Model): def details_display(self, **kwargs): result = super().details_display(**kwargs) - result['display'].insert(2, (_('Slug'), str(self.get_slug()))) + result['display'].insert(2, (_('Slug'), self.get_slug())) return result @cached_property @@ -120,7 +120,7 @@ class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model): def _serialize(self, search=False, **kwargs): result = super()._serialize(**kwargs) - result['subtitle'] = str(self.subtitle) + result['subtitle'] = self.subtitle result['icon'] = self.get_icon() result['can_search'] = self.can_search result['can_describe'] = self.can_search @@ -202,7 +202,7 @@ class SpecificLocation(Location, models.Model): result['label_settings'] = label_settings.serialize(detailed=False) if self.label_overrides: # todo: what if only one language is set? - result['label_override'] = str(self.label_override) + result['label_override'] = self.label_override return result def get_label_settings(self): @@ -300,9 +300,9 @@ class LocationGroupCategory(SerializableMixin, models.Model): result['titles'] = self.titles result['titles_plural'] = self.titles_plural result['help_texts'] = self.help_texts - result['title'] = str(self.title) - result['title_plural'] = str(self.title_plural) - result['help_text'] = str(self.help_text) + result['title'] = self.title + result['title_plural'] = self.title_plural + result['help_text'] = self.help_text result['allow_levels'] = self.allow_levels result['allow_spaces'] = self.allow_spaces result['allow_areas'] = self.allow_areas diff --git a/src/c3nav/mapdata/schemas/filters.py b/src/c3nav/mapdata/schemas/filters.py index ef54ccc6..6395bd81 100644 --- a/src/c3nav/mapdata/schemas/filters.py +++ b/src/c3nav/mapdata/schemas/filters.py @@ -2,10 +2,10 @@ from typing import Literal, Optional, Type from django.core.cache import cache from django.db.models import Model, QuerySet -from ninja import Schema from pydantic import Field as APIField from c3nav.api.exceptions import APIRequestValidationFailed +from c3nav.api.schema import BaseSchema from c3nav.mapdata.models import Level, LocationGroup, LocationGroupCategory, MapUpdate, Space from c3nav.mapdata.models.access import AccessPermission @@ -38,7 +38,7 @@ def assert_valid_value(request, model: Type[Model], key: str, values: set): raise APIRequestValidationFailed("Unknown %s: %r" % (model.__name__, remainder)) -class FilterSchema(Schema): +class FilterSchema(BaseSchema): def filter_qs(self, qs: QuerySet) -> QuerySet: return qs diff --git a/src/c3nav/mapdata/schemas/model_base.py b/src/c3nav/mapdata/schemas/model_base.py index 3d12752d..c4e06a06 100644 --- a/src/c3nav/mapdata/schemas/model_base.py +++ b/src/c3nav/mapdata/schemas/model_base.py @@ -1,13 +1,11 @@ import re -from typing import Annotated, Any, Optional, Union +from typing import Annotated, Optional, Union from ninja import Schema from pydantic import Field as APIField -from pydantic import PositiveInt, model_validator -from pydantic.functional_validators import ModelWrapValidatorHandler -from pydantic_core.core_schema import ValidationInfo +from pydantic import PositiveInt -from c3nav.api.schema import LineStringSchema, PointSchema, PolygonSchema +from c3nav.api.schema import LineStringSchema, PointSchema, PolygonSchema, BaseSchema from c3nav.api.utils import NonEmptyStr @@ -36,24 +34,14 @@ BoundsSchema = tuple[ ] -class SerializableSchema(Schema): - @model_validator(mode="wrap") # noqa - @classmethod - def _run_root_validator(cls, values: Any, handler: ModelWrapValidatorHandler[Schema], info: ValidationInfo) -> Any: - """ overwriting this, we need to call serialize to get the correct data """ - if hasattr(values, 'serialize') and callable(values.serialize): - values = values.serialize() - return handler(values) - - -class DjangoModelSchema(SerializableSchema): +class DjangoModelSchema(BaseSchema): id: PositiveInt = APIField( title="ID", example=1, ) -class LocationSlugSchema(Schema): +class LocationSlugSchema(BaseSchema): slug: NonEmptyStr = APIField( title="location slug", description="a slug is a unique way to refer to a location. while locations have a shared ID space, slugs" @@ -65,7 +53,7 @@ class LocationSlugSchema(Schema): ) -class WithAccessRestrictionSchema(Schema): +class WithAccessRestrictionSchema(BaseSchema): access_restriction: Union[ Annotated[PositiveInt, APIField(title="access restriction ID")], Annotated[None, APIField(title="null", description="no access restriction")], @@ -76,7 +64,7 @@ class WithAccessRestrictionSchema(Schema): ) -class TitledSchema(Schema): +class TitledSchema(BaseSchema): titles: dict[NonEmptyStr, NonEmptyStr] = APIField( title="title (all languages)", description="title in all available languages. property names are the ISO-language code. " @@ -213,7 +201,7 @@ class SpecificLocationSchema(LocationSchema): ) -class WithPolygonGeometrySchema(Schema): +class WithPolygonGeometrySchema(BaseSchema): geometry: Union[ PolygonSchema, Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")] @@ -224,7 +212,7 @@ class WithPolygonGeometrySchema(Schema): ) -class WithLineStringGeometrySchema(Schema): +class WithLineStringGeometrySchema(BaseSchema): geometry: Union[ LineStringSchema, Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")] @@ -235,7 +223,7 @@ class WithLineStringGeometrySchema(Schema): ) -class WithPointGeometrySchema(Schema): +class WithPointGeometrySchema(BaseSchema): geometry: Union[ PointSchema, Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")] @@ -246,7 +234,7 @@ class WithPointGeometrySchema(Schema): ) -class WithLevelSchema(SerializableSchema): +class WithLevelSchema(BaseSchema): level: PositiveInt = APIField( title="level", description="level id this object belongs to.", @@ -254,7 +242,7 @@ class WithLevelSchema(SerializableSchema): ) -class WithSpaceSchema(SerializableSchema): +class WithSpaceSchema(BaseSchema): space: PositiveInt = APIField( title="space", description="space id this object belongs to.", @@ -262,7 +250,7 @@ class WithSpaceSchema(SerializableSchema): ) -class SimpleGeometryPointSchema(Schema): +class SimpleGeometryPointSchema(BaseSchema): point: tuple[ Annotated[PositiveInt, APIField(title="level ID")], Annotated[float, APIField(title="x coordinate")], @@ -281,7 +269,7 @@ class SimpleGeometryPointAndBoundsSchema(SimpleGeometryPointSchema): ) -class SimpleGeometryLocationsSchema(Schema): +class SimpleGeometryLocationsSchema(BaseSchema): locations: list[PositiveInt] = APIField( # todo: this should be a set… but json serialization? description="IDs of all locations that belong to this grouo", example=(1, 2, 3), diff --git a/src/c3nav/mapdata/schemas/models.py b/src/c3nav/mapdata/schemas/models.py index 439097c8..1a651f45 100644 --- a/src/c3nav/mapdata/schemas/models.py +++ b/src/c3nav/mapdata/schemas/models.py @@ -5,10 +5,10 @@ from pydantic import Discriminator from pydantic import Field as APIField from pydantic import NonNegativeFloat, PositiveFloat, PositiveInt -from c3nav.api.schema import GeometrySchema, PointSchema +from c3nav.api.schema import GeometrySchema, PointSchema, BaseSchema from c3nav.api.utils import NonEmptyStr from c3nav.mapdata.schemas.model_base import (AnyLocationID, AnyPositionID, CustomLocationID, DjangoModelSchema, - LabelSettingsSchema, LocationSchema, PositionID, SerializableSchema, + LabelSettingsSchema, LocationSchema, PositionID, SimpleGeometryLocationsSchema, SimpleGeometryPointAndBoundsSchema, SimpleGeometryPointSchema, SpecificLocationSchema, TitledSchema, WithAccessRestrictionSchema, WithLevelSchema, @@ -357,7 +357,7 @@ class AccessRestrictionGroupSchema(WithAccessRestrictionSchema, DjangoModelSchem pass -class CustomLocationSchema(SerializableSchema): +class CustomLocationSchema(BaseSchema): """ A custom location represents coordinates that have been put in or calculated. @@ -439,7 +439,7 @@ class CustomLocationSchema(SerializableSchema): ) -class TrackablePositionSchema(Schema): +class TrackablePositionSchema(BaseSchema): """ A trackable position. Its position can be set or reset. """ @@ -468,7 +468,7 @@ class TrackablePositionSchema(Schema): ) -class LocationTypeSchema(Schema): +class LocationTypeSchema(BaseSchema): locationtype: str = APIField(title="location type", description="indicates what kind of location is included. " "different location types have different fields.") @@ -544,7 +544,7 @@ class TrackablePositionLocationSchema(TrackablePositionSchema, LocationTypeSchem locationtype: Literal["position"] = LocationTypeAPIField() -class SlimLocationMixin(Schema): +class SlimLocationMixin(BaseSchema): level: ClassVar[None] space: ClassVar[None] titles: ClassVar[None] @@ -662,7 +662,7 @@ all_location_definitions = listable_location_definitions + "\n" + schema_definit ) -class DisplayLink(Schema): +class DisplayLink(BaseSchema): """ A link for the location display """ @@ -672,7 +672,7 @@ class DisplayLink(Schema): can_search: bool -class LocationDisplay(SerializableSchema): +class LocationDisplay(BaseSchema): id: AnyLocationID = APIField( description="a numeric ID for a map location or a string ID for generated locations", example=1, @@ -749,7 +749,7 @@ class LocationDisplay(SerializableSchema): ) -class PositionStatusSchema(Schema): +class PositionStatusSchema(BaseSchema): id: AnyPositionID = APIField( description="the ID of the dynamic position that has been queries", ) @@ -758,7 +758,7 @@ class PositionStatusSchema(Schema): ) -class PositionAvailabilitySchema(Schema): +class PositionAvailabilitySchema(BaseSchema): available: str diff --git a/src/c3nav/mapdata/schemas/responses.py b/src/c3nav/mapdata/schemas/responses.py index 099ead74..3dba020f 100644 --- a/src/c3nav/mapdata/schemas/responses.py +++ b/src/c3nav/mapdata/schemas/responses.py @@ -1,14 +1,13 @@ from typing import Annotated, Union -from ninja import Schema from pydantic import Field as APIField from pydantic import PositiveInt -from c3nav.api.schema import GeometrySchema +from c3nav.api.schema import GeometrySchema, BaseSchema from c3nav.mapdata.schemas.model_base import AnyLocationID, BoundsSchema -class WithBoundsSchema(Schema): +class WithBoundsSchema(BaseSchema): """ Describing a bounding box """ @@ -18,7 +17,7 @@ class WithBoundsSchema(Schema): ) -class LocationGeometry(Schema): +class LocationGeometry(BaseSchema): id: AnyLocationID = APIField( description="ID of the location that the geometry is being queried for", ) diff --git a/src/c3nav/mapdata/utils/locations.py b/src/c3nav/mapdata/utils/locations.py index 3d5ea65c..90995a1f 100644 --- a/src/c3nav/mapdata/utils/locations.py +++ b/src/c3nav/mapdata/utils/locations.py @@ -416,7 +416,7 @@ class CustomLocation: @cached_property def grid_square(self): - return grid.get_square_for_point(self.x, self.y) or '' + return grid.get_square_for_point(self.x, self.y) @cached_property def title_subtitle(self): diff --git a/src/c3nav/mapdata/utils/user.py b/src/c3nav/mapdata/utils/user.py index 55c7b01d..2e5d402d 100644 --- a/src/c3nav/mapdata/utils/user.py +++ b/src/c3nav/mapdata/utils/user.py @@ -17,13 +17,13 @@ def get_user_data(request): } if permissions: result.update({ - 'title': str(_('not logged in')), + 'title': _('not logged in'), 'subtitle': ngettext_lazy('%d area unlocked', '%d areas unlocked', len(permissions)) % len(permissions), 'permissions': tuple(permissions), }) else: result.update({ - 'title': str(_('Login')), + 'title': _('Login'), 'subtitle': None, 'permissions': (), }) diff --git a/src/c3nav/mesh/api.py b/src/c3nav/mesh/api.py index cda708cd..b3aa7846 100644 --- a/src/c3nav/mesh/api.py +++ b/src/c3nav/mesh/api.py @@ -11,6 +11,7 @@ from pydantic import PositiveInt, field_validator from c3nav.api.auth import APIKeyAuth, auth_permission_responses, auth_responses, validate_responses from c3nav.api.exceptions import API404, APIConflict, APIRequestValidationFailed +from c3nav.api.schema import BaseSchema from c3nav.mesh.dataformats import BoardType, ChipType, FirmwareImage from c3nav.mesh.messages import MeshMessageType from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, NodeMessage @@ -18,7 +19,7 @@ from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, NodeMessage mesh_api_router = APIRouter(tags=["mesh"], auth=APIKeyAuth(permissions={"mesh_control"})) -class FirmwareBuildSchema(Schema): +class FirmwareBuildSchema(BaseSchema): """ A build belonging to a firmware version. """ @@ -53,7 +54,7 @@ class FirmwareBuildSchema(Schema): pass -class FirmwareSchema(Schema): +class FirmwareSchema(BaseSchema): """ A firmware version, usually with multiple build variants. """ @@ -128,7 +129,7 @@ def firmware_project_description(request, firmware_id: int, variant: str): raise API404("Firmware or firmware build not found") -class UploadFirmwareBuildSchema(Schema): +class UploadFirmwareBuildSchema(BaseSchema): """ A firmware build to upload, with at least one build variant """ @@ -138,7 +139,7 @@ class UploadFirmwareBuildSchema(Schema): uploaded_filename: str = APIField(..., example="firmware.bin") -class UploadFirmwareSchema(Schema): +class UploadFirmwareSchema(BaseSchema): """ A firmware version to upload, with at least one build variant """ @@ -205,14 +206,14 @@ def firmware_upload(request, firmware_data: UploadFirmwareSchema, binary_files: NodeAddress = Annotated[str, APIField(pattern=r"^[a-z0-9]{2}(:[a-z0-9]{2}){5}$")] -class MessagesFilter(Schema): +class MessagesFilter(BaseSchema): src_node: Optional[NodeAddress] = None msg_type: Optional[MeshMessageType] = None time_from: Optional[datetime] = None time_until: Optional[datetime] = None -class NodeMessageSchema(Schema): +class NodeMessageSchema(BaseSchema): id: int src_node: NodeAddress message_type: MeshMessageType diff --git a/src/c3nav/routing/api/positioning.py b/src/c3nav/routing/api/positioning.py index 7f9c06f6..434cdb9a 100644 --- a/src/c3nav/routing/api/positioning.py +++ b/src/c3nav/routing/api/positioning.py @@ -3,9 +3,9 @@ from typing import Annotated, Union from django.core.exceptions import ValidationError from ninja import Field as APIField from ninja import Router as APIRouter -from ninja import Schema from c3nav.api.auth import auth_responses +from c3nav.api.schema import BaseSchema from c3nav.mapdata.models.access import AccessPermission from c3nav.mapdata.schemas.models import CustomLocationSchema from c3nav.mapdata.utils.cache.stats import increment_cache_key @@ -15,13 +15,13 @@ from c3nav.routing.schemas import BSSIDSchema, LocateRequestPeerSchema positioning_api_router = APIRouter(tags=["positioning"]) -class LocateRequestSchema(Schema): +class LocateRequestSchema(BaseSchema): peers: list[LocateRequestPeerSchema] = APIField( title="list of visible/measured location beacons", ) -class PositioningResult(Schema): +class PositioningResult(BaseSchema): location: Union[ Annotated[CustomLocationSchema, APIField(title="location")], Annotated[None, APIField(title="null", description="position could not be determined")] diff --git a/src/c3nav/routing/api/routing.py b/src/c3nav/routing/api/routing.py index 60744284..1c4aa5c1 100644 --- a/src/c3nav/routing/api/routing.py +++ b/src/c3nav/routing/api/routing.py @@ -11,6 +11,7 @@ from pydantic import PositiveInt from c3nav.api.auth import APIKeyAuth, auth_responses, validate_responses from c3nav.api.exceptions import APIRequestValidationFailed +from c3nav.api.schema import BaseSchema from c3nav.api.utils import NonEmptyStr from c3nav.mapdata.api.base import api_stats_clean_location_value from c3nav.mapdata.models.access import AccessPermission @@ -53,7 +54,7 @@ class AltitudeWayTypeChoice(StrEnum): AVOID = "avoid" -class UpdateRouteOptionsSchema(Schema): +class UpdateRouteOptionsSchema(BaseSchema): # todo: default is wrong, this should be optional mode: Union[ Annotated[RouteMode, APIField(title="route mode", description="routing mode to use")], @@ -81,7 +82,7 @@ class UpdateRouteOptionsSchema(Schema): ) -class RouteOptionsSchema(Schema): +class RouteOptionsSchema(BaseSchema): # todo: default is wrong, this should be optional mode: RouteMode = APIField(name="routing mode") walk_speed: WalkSpeed = APIField(name="walk speed") @@ -96,7 +97,7 @@ class RouteOptionsSchema(Schema): ) -class RouteParametersSchema(Schema): +class RouteParametersSchema(BaseSchema): origin: AnyLocationID destination: AnyLocationID options_override: Optional[UpdateRouteOptionsSchema] = APIField( @@ -105,7 +106,7 @@ class RouteParametersSchema(Schema): ) -class RouteItemSchema(Schema): +class RouteItemSchema(BaseSchema): id: PositiveInt coordinates: Coordinates3D waytype: Union[ @@ -132,7 +133,7 @@ class RouteItemSchema(Schema): ]] -class RouteSchema(Schema): +class RouteSchema(BaseSchema): origin: dict # todo: improve this destination: dict # todo: improve this distance: float @@ -144,7 +145,7 @@ class RouteSchema(Schema): items: list[RouteItemSchema] -class RouteResponse(Schema): +class RouteResponse(BaseSchema): request: RouteParametersSchema options: RouteOptionsSchema report_issue_url: NonEmptyStr @@ -154,7 +155,7 @@ class RouteResponse(Schema): title = "route found" -class NoRouteResponse(Schema): +class NoRouteResponse(BaseSchema): request: RouteParametersSchema options: RouteOptionsSchema error: NonEmptyStr = APIField( @@ -197,19 +198,19 @@ def get_route(request, parameters: RouteParametersSchema): return NoRouteResponse( request=parameters, options=_new_serialize_route_options(options), - error=str(_('Not yet routable, try again shortly.')), + error=_('Not yet routable, try again shortly.'), ) except LocationUnreachable: return NoRouteResponse( request=parameters, options=_new_serialize_route_options(options), - error=str(_('Unreachable location.')) + error=_('Unreachable location.') ) except NoRouteFound: return NoRouteResponse( request=parameters, options=_new_serialize_route_options(options), - error=str(_('No route found.')) + error=_('No route found.') ) origin_values = api_stats_clean_location_value(form.cleaned_data['origin'].pk) @@ -282,12 +283,12 @@ def set_route_options(request, new_options: UpdateRouteOptionsSchema): return _new_serialize_route_options(options) -class RouteOptionsFieldChoices(Schema): +class RouteOptionsFieldChoices(BaseSchema): name: NonEmptyStr title: NonEmptyStr -class RouteOptionsField(Schema): +class RouteOptionsField(BaseSchema): name: NonEmptyStr type: NonEmptyStr label: NonEmptyStr diff --git a/src/c3nav/routing/route.py b/src/c3nav/routing/route.py index e9fb1743..4be73f42 100644 --- a/src/c3nav/routing/route.py +++ b/src/c3nav/routing/route.py @@ -240,9 +240,7 @@ class RouteItem: if self.new_level: result['level'] = describe_location(self.level, locations) - # convert all the string proxies to strings - # todo: better to let the api response serializer do this somehow? - result['descriptions'] = [(icon, str(instruction)) for (icon, instruction) in self.descriptions] + result['descriptions'] = [(icon, instruction) for (icon, instruction) in self.descriptions] return result diff --git a/src/c3nav/routing/schemas.py b/src/c3nav/routing/schemas.py index 619cec9f..c68050d0 100644 --- a/src/c3nav/routing/schemas.py +++ b/src/c3nav/routing/schemas.py @@ -1,15 +1,15 @@ from typing import Annotated, Union -from ninja import Schema from pydantic import Field as APIField from pydantic import NegativeInt, PositiveInt +from c3nav.api.schema import BaseSchema from c3nav.api.utils import NonEmptyStr BSSIDSchema = Annotated[str, APIField(pattern=r"^[a-z0-9]{2}(:[a-z0-9]{2}){5}$", title="BSSID")] -class LocateRequestPeerSchema(Schema): +class LocateRequestPeerSchema(BaseSchema): bssid: BSSIDSchema = APIField( title="BSSID", description="BSSID of the peer",