drfast-api 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
drfast/pagination.py ADDED
@@ -0,0 +1,53 @@
1
+ """Native JSON limit/offset pagination."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import HTTPException, Request, status
8
+
9
+
10
+ class LimitOffsetPagination:
11
+ default_limit = 50
12
+ max_limit = 100
13
+
14
+ def parameters(self, request: Request) -> tuple[int, int]:
15
+ try:
16
+ limit = int(request.query_params.get("limit", self.default_limit))
17
+ offset = int(request.query_params.get("offset", 0))
18
+ except ValueError as exc:
19
+ raise HTTPException(
20
+ status_code=status.HTTP_400_BAD_REQUEST,
21
+ detail="Invalid pagination.",
22
+ ) from exc
23
+ if limit < 1 or limit > self.max_limit or offset < 0:
24
+ raise HTTPException(
25
+ status_code=status.HTTP_400_BAD_REQUEST,
26
+ detail="Invalid pagination.",
27
+ )
28
+ return limit, offset
29
+
30
+ def response(
31
+ self,
32
+ request: Request,
33
+ *,
34
+ count: int,
35
+ offset: int,
36
+ limit: int,
37
+ results: list[Any],
38
+ ) -> dict[str, Any]:
39
+ def link(next_offset: int) -> str | None:
40
+ if next_offset < 0 or next_offset >= count:
41
+ return None
42
+ params = dict(request.query_params)
43
+ params.update({"limit": str(limit), "offset": str(next_offset)})
44
+ query = "&".join(f"{key}={value}" for key, value in params.items())
45
+ return f"{request.url.path}?{query}"
46
+
47
+ return {
48
+ "count": count,
49
+ "next": link(offset + limit),
50
+ "previous": link(max(offset - limit, -1)),
51
+ "results": results,
52
+ }
53
+
drfast/permissions.py ADDED
@@ -0,0 +1,54 @@
1
+ """Async DRF-style permission primitives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import HTTPException, Request, status
8
+
9
+
10
+ class BasePermission:
11
+ """Base for request-level and object-level access rules."""
12
+
13
+ message = "You do not have permission to perform this action."
14
+
15
+ async def has_permission(self, request: Request, view: Any) -> bool:
16
+ return True
17
+
18
+ async def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
19
+ return await self.has_permission(request, view)
20
+
21
+
22
+ class AllowAny(BasePermission):
23
+ pass
24
+
25
+
26
+ class IsAuthenticated(BasePermission):
27
+ message = "Authentication credentials were not provided."
28
+
29
+ async def has_permission(self, request: Request, view: Any) -> bool:
30
+ user = getattr(request.state, "user", None)
31
+ return user is not None and bool(getattr(user, "is_active", True))
32
+
33
+
34
+ class IsAdminUser(IsAuthenticated):
35
+ message = "Administrator access is required."
36
+
37
+ async def has_permission(self, request: Request, view: Any) -> bool:
38
+ return await super().has_permission(request, view) and bool(
39
+ getattr(request.state.user, "is_superuser", False)
40
+ )
41
+
42
+
43
+ async def enforce_permissions(request: Request, view: Any, obj: Any | None = None) -> None:
44
+ """Raise a uniform HTTP error for failed configured permissions."""
45
+ for permission_type in getattr(view, "permission_classes", (AllowAny,)):
46
+ permission = permission_type()
47
+ allowed = (
48
+ await permission.has_object_permission(request, view, obj)
49
+ if obj is not None
50
+ else await permission.has_permission(request, view)
51
+ )
52
+ if not allowed:
53
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=permission.message)
54
+
drfast/routers.py ADDED
@@ -0,0 +1,42 @@
1
+ """Router registry for model viewsets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter, FastAPI
8
+
9
+ from drfast.views import ModelViewSet
10
+
11
+
12
+ class DefaultRouter:
13
+ """Register viewsets then mount their FastAPI routes explicitly."""
14
+
15
+ def __init__(self) -> None:
16
+ self._registry: list[tuple[str, type[ModelViewSet], str | None]] = []
17
+
18
+ def register(
19
+ self,
20
+ prefix: str,
21
+ viewset: type[ModelViewSet],
22
+ *,
23
+ basename: str | None = None,
24
+ ) -> None:
25
+ self._registry.append((prefix.rstrip("/"), viewset, basename))
26
+
27
+ def urls(self) -> list[APIRouter]:
28
+ return [
29
+ viewset().as_router(prefix=prefix, tags=[basename or prefix])
30
+ for prefix, viewset, basename in self._registry
31
+ ]
32
+
33
+ def include_in(self, app: FastAPI, *, prefix: str = "") -> None:
34
+ for router in self.urls():
35
+ app.include_router(router, prefix=prefix)
36
+
37
+ def routes(self) -> list[dict[str, Any]]:
38
+ return [
39
+ {"prefix": prefix, "viewset": viewset.__name__, "basename": basename}
40
+ for prefix, viewset, basename in self._registry
41
+ ]
42
+
drfast/serializers.py ADDED
@@ -0,0 +1,214 @@
1
+ """DRF-shaped serializers backed by Pydantic v2 validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any, ClassVar, Generic, TypeVar, cast
7
+
8
+ from pydantic import (
9
+ BaseModel,
10
+ ConfigDict,
11
+ EmailStr,
12
+ ValidationError,
13
+ create_model,
14
+ )
15
+ from pydantic import (
16
+ Field as PydanticField,
17
+ )
18
+ from tortoise.models import Model as TortoiseModel
19
+
20
+ ModelType = TypeVar("ModelType", bound=TortoiseModel)
21
+
22
+
23
+ class SerializerValidationError(ValueError):
24
+ """Validation failure with a JSON-ready error detail."""
25
+
26
+ def __init__(self, detail: Any) -> None:
27
+ super().__init__("Serializer validation failed")
28
+ self.detail = detail
29
+
30
+
31
+ class Field:
32
+ """A declarative serializer field.
33
+
34
+ The field's concrete Python type is passed to Pydantic, so FastAPI retains
35
+ native request validation and OpenAPI schemas.
36
+ """
37
+
38
+ python_type: Any = Any
39
+
40
+ def __init__(
41
+ self,
42
+ *,
43
+ required: bool = True,
44
+ default: Any = ...,
45
+ read_only: bool = False,
46
+ write_only: bool = False,
47
+ alias: str | None = None,
48
+ ) -> None:
49
+ self.required = required
50
+ self.default = default
51
+ self.read_only = read_only
52
+ self.write_only = write_only
53
+ self.alias = alias
54
+
55
+ def pydantic_field(self, *, for_input: bool) -> tuple[Any, Any]:
56
+ if (for_input and self.read_only) or (not for_input and self.write_only):
57
+ return Any, PydanticField(exclude=True)
58
+ default = self.default
59
+ if not self.required and default is ...:
60
+ default = None
61
+ return self.python_type, PydanticField(default=default, alias=self.alias)
62
+
63
+
64
+ class CharField(Field):
65
+ python_type = str
66
+
67
+
68
+ class EmailField(Field):
69
+ python_type = EmailStr
70
+
71
+
72
+ class IntegerField(Field):
73
+ python_type = int
74
+
75
+
76
+ class BooleanField(Field):
77
+ python_type = bool
78
+
79
+
80
+ class UUIDField(Field):
81
+ from uuid import UUID
82
+
83
+ python_type = UUID
84
+
85
+
86
+ class SerializerMeta(type):
87
+ def __new__(
88
+ mcls: type[SerializerMeta],
89
+ name: str,
90
+ bases: tuple[type[Any], ...],
91
+ namespace: dict[str, Any],
92
+ ) -> type[Serializer[Any]]:
93
+ declared: dict[str, Field] = {}
94
+ for base in bases:
95
+ declared.update(getattr(base, "_declared_fields", {}))
96
+ for key, value in list(namespace.items()):
97
+ if isinstance(value, Field):
98
+ declared[key] = value
99
+ namespace.pop(key)
100
+ namespace["_declared_fields"] = declared
101
+ return cast("type[Serializer[Any]]", super().__new__(mcls, name, bases, namespace))
102
+
103
+
104
+ class Serializer(Generic[ModelType], metaclass=SerializerMeta):
105
+ """Serializer with explicit fields and async validation hooks."""
106
+
107
+ _declared_fields: ClassVar[dict[str, Field]]
108
+
109
+ def __init__(self, instance: ModelType | None = None, *, partial: bool = False) -> None:
110
+ self.instance = instance
111
+ self.partial = partial
112
+ self.validated_data: dict[str, Any] = {}
113
+
114
+ @classmethod
115
+ def _schema(cls, *, for_input: bool, partial: bool = False) -> type[BaseModel]:
116
+ fields: dict[str, tuple[Any, Any]] = {}
117
+ for name, field in cls._declared_fields.items():
118
+ if (for_input and field.read_only) or (not for_input and field.write_only):
119
+ continue
120
+ annotation, default = field.pydantic_field(for_input=for_input)
121
+ if partial and for_input and default is ...:
122
+ default = None
123
+ fields[name] = (annotation, default)
124
+ model = create_model(
125
+ f"{cls.__name__}{'Input' if for_input else 'Output'}",
126
+ __config__=ConfigDict(from_attributes=True, populate_by_name=True),
127
+ **cast(Any, fields),
128
+ )
129
+ return cast(type[BaseModel], model)
130
+
131
+ @classmethod
132
+ def input_schema(cls) -> type[BaseModel]:
133
+ return cls._schema(for_input=True)
134
+
135
+ @classmethod
136
+ def output_schema(cls) -> type[BaseModel]:
137
+ return cls._schema(for_input=False)
138
+
139
+ async def is_valid(self, data: Mapping[str, Any]) -> bool:
140
+ try:
141
+ parsed = self._schema(for_input=True, partial=self.partial).model_validate(data)
142
+ except ValidationError as exc:
143
+ raise SerializerValidationError(exc.errors(include_url=False)) from exc
144
+ values = parsed.model_dump(exclude_unset=self.partial, by_alias=False)
145
+ for name, value in list(values.items()):
146
+ validator = getattr(self, f"validate_{name}", None)
147
+ if validator is not None:
148
+ result = validator(value)
149
+ values[name] = await result if hasattr(result, "__await__") else result
150
+ validated = self.validate(values)
151
+ self.validated_data = await validated if hasattr(validated, "__await__") else validated
152
+ return True
153
+
154
+ def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
155
+ return attrs
156
+
157
+ async def create(self, validated_data: dict[str, Any]) -> ModelType:
158
+ raise NotImplementedError("Serializer.create() must be implemented")
159
+
160
+ async def update(self, instance: ModelType, validated_data: dict[str, Any]) -> ModelType:
161
+ raise NotImplementedError("Serializer.update() must be implemented")
162
+
163
+ async def save(self) -> ModelType:
164
+ if self.instance is None:
165
+ self.instance = await self.create(self.validated_data)
166
+ else:
167
+ self.instance = await self.update(self.instance, self.validated_data)
168
+ return self.instance
169
+
170
+ def representation(self, instance: ModelType | Mapping[str, Any]) -> dict[str, Any]:
171
+ data = self.output_schema().model_validate(instance).model_dump(mode="json", by_alias=True)
172
+ return {name: value for name, value in data.items() if value is not None}
173
+
174
+
175
+ class ModelSerializer(Serializer[ModelType]):
176
+ """Serializer with model-backed create and update behavior."""
177
+
178
+ model: ClassVar[type[ModelType]]
179
+ fields: ClassVar[tuple[str, ...] | str] = "__all__"
180
+ read_only_fields: ClassVar[tuple[str, ...]] = ()
181
+
182
+ @classmethod
183
+ def _model_fields(cls) -> dict[str, Field]:
184
+ if cls._declared_fields:
185
+ return cls._declared_fields
186
+ result: dict[str, Field] = {}
187
+ selected = None if cls.fields == "__all__" else set(cls.fields)
188
+ for name, db_field in cls.model._meta.fields_map.items():
189
+ if selected is not None and name not in selected:
190
+ continue
191
+ is_automatic = name in {"id", "created_at", "updated_at", "deleted_at"}
192
+ python_type = getattr(db_field, "field_type", Any)
193
+ result[name] = Field(
194
+ required=not getattr(db_field, "null", False),
195
+ read_only=is_automatic or name in cls.read_only_fields,
196
+ )
197
+ result[name].python_type = python_type
198
+ return result
199
+
200
+ @classmethod
201
+ def _schema(cls, *, for_input: bool, partial: bool = False) -> type[BaseModel]:
202
+ if not cls._declared_fields:
203
+ cls._declared_fields = cls._model_fields()
204
+ return super()._schema(for_input=for_input, partial=partial)
205
+
206
+ async def create(self, validated_data: dict[str, Any]) -> ModelType:
207
+ return await self.model.create(**validated_data)
208
+
209
+ async def update(self, instance: ModelType, validated_data: dict[str, Any]) -> ModelType:
210
+ for name, value in validated_data.items():
211
+ setattr(instance, name, value)
212
+ await instance.save(update_fields=list(validated_data))
213
+ return instance
214
+
drfast/settings.py ADDED
@@ -0,0 +1,45 @@
1
+ """Typed application settings and Tortoise configuration generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import Field, SecretStr
8
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
+
10
+
11
+ class DRFastSettings(BaseSettings):
12
+ """Settings consumed by project wiring, never by views directly."""
13
+
14
+ model_config = SettingsConfigDict(env_file=".env", env_prefix="", extra="ignore")
15
+
16
+ app_name: str = "DRFast API"
17
+ debug: bool = False
18
+ database_url: str = "sqlite://db.sqlite3"
19
+ installed_apps: list[str] = Field(default_factory=list)
20
+ secret_key: SecretStr = SecretStr("change-me-in-production")
21
+ jwt_lifetime_seconds: int = 3600
22
+ cors_origins: list[str] = Field(default_factory=list)
23
+
24
+ def tortoise_config(self) -> dict[str, Any]:
25
+ """Return the configuration shared by runtime and migration commands."""
26
+ apps: dict[str, dict[str, Any]] = {}
27
+ for module in self.installed_apps:
28
+ package, _, _ = module.rpartition(".")
29
+ label = package.rsplit(".", maxsplit=1)[-1]
30
+ if label in apps:
31
+ raise ValueError(f"Duplicate Tortoise app label '{label}' in installed_apps")
32
+ apps[label] = {
33
+ "models": [module],
34
+ "default_connection": "default",
35
+ "migrations": f"{package}.migrations",
36
+ }
37
+ return {
38
+ "connections": {"default": self.database_url},
39
+ "apps": apps,
40
+ }
41
+
42
+ def validate_production(self) -> None:
43
+ if not self.debug and self.secret_key.get_secret_value() == "change-me-in-production":
44
+ raise ValueError("SECRET_KEY must be set when DEBUG is false")
45
+
drfast/views.py ADDED
@@ -0,0 +1,230 @@
1
+ """Class-based FastAPI views and Tortoise model viewsets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Callable, Sequence
7
+ from typing import Any, ClassVar, cast
8
+ from uuid import UUID
9
+
10
+ from fastapi import APIRouter, Body, Depends, HTTPException, Request, status
11
+ from tortoise.exceptions import DoesNotExist
12
+ from tortoise.models import Model as TortoiseModel
13
+
14
+ from drfast.filters import FilterSet
15
+ from drfast.pagination import LimitOffsetPagination
16
+ from drfast.permissions import AllowAny, BasePermission, enforce_permissions
17
+ from drfast.serializers import ModelSerializer, SerializerValidationError
18
+
19
+
20
+ class APIView:
21
+ """Base class for a FastAPI route group with DRF-style permissions."""
22
+
23
+ permission_classes: ClassVar[tuple[type[BasePermission], ...]] = (AllowAny,)
24
+ authentication_dependencies: ClassVar[tuple[Callable[..., Any], ...]] = ()
25
+
26
+ async def check_permissions(self, request: Request, obj: Any | None = None) -> None:
27
+ await enforce_permissions(request, self, obj)
28
+
29
+
30
+ class ModelViewSet(APIView):
31
+ """Async CRUD viewset for a Tortoise model."""
32
+
33
+ model: ClassVar[type[TortoiseModel]]
34
+ serializer_class: ClassVar[type[ModelSerializer[Any]]]
35
+ filterset_class: ClassVar[type[FilterSet] | None] = None
36
+ pagination_class: ClassVar[type[LimitOffsetPagination] | None] = LimitOffsetPagination
37
+ ordering_fields: ClassVar[tuple[str, ...]] = ()
38
+
39
+ def serializer(
40
+ self,
41
+ instance: Any | None = None,
42
+ *,
43
+ partial: bool = False,
44
+ ) -> ModelSerializer[Any]:
45
+ return self.serializer_class(instance, partial=partial)
46
+
47
+ async def get_object(self, request: Request, pk: UUID) -> TortoiseModel:
48
+ try:
49
+ obj = await self.model.get(id=pk)
50
+ except DoesNotExist as exc:
51
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found.") from exc
52
+ await self.check_permissions(request, obj)
53
+ return obj
54
+
55
+ def _queryset(self, request: Request) -> Any:
56
+ queryset = self.model.all()
57
+ if self.filterset_class is not None:
58
+ queryset = queryset.filter(**self.filterset_class.filters_from_request(request))
59
+ ordering = request.query_params.get("ordering")
60
+ if ordering:
61
+ fields = tuple(field.strip() for field in ordering.split(",") if field.strip())
62
+ if not fields or any(
63
+ field.removeprefix("-") not in self.ordering_fields for field in fields
64
+ ):
65
+ raise HTTPException(
66
+ status_code=status.HTTP_400_BAD_REQUEST,
67
+ detail="Invalid ordering.",
68
+ )
69
+ queryset = queryset.order_by(*fields)
70
+ return queryset
71
+
72
+ async def list(self, request: Request) -> dict[str, Any] | list[dict[str, Any]]:
73
+ await self.check_permissions(request)
74
+ queryset = self._queryset(request)
75
+ if self.pagination_class is None:
76
+ instances = await queryset
77
+ return [self.serializer(instance).representation(instance) for instance in instances]
78
+ paginator = self.pagination_class()
79
+ limit, offset = paginator.parameters(request)
80
+ count = await queryset.count()
81
+ instances = await queryset.offset(offset).limit(limit)
82
+ return paginator.response(
83
+ request,
84
+ count=count,
85
+ offset=offset,
86
+ limit=limit,
87
+ results=[
88
+ self.serializer(instance).representation(instance) for instance in instances
89
+ ],
90
+ )
91
+
92
+ async def retrieve(self, request: Request, pk: UUID) -> dict[str, Any]:
93
+ instance = await self.get_object(request, pk)
94
+ return self.serializer(instance).representation(instance)
95
+
96
+ async def create(self, request: Request, payload: dict[str, Any]) -> dict[str, Any]:
97
+ await self.check_permissions(request)
98
+ serializer = self.serializer()
99
+ try:
100
+ await serializer.is_valid(payload)
101
+ except SerializerValidationError as exc:
102
+ raise HTTPException(
103
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
104
+ detail=exc.detail,
105
+ ) from exc
106
+ instance = await serializer.save()
107
+ return serializer.representation(instance)
108
+
109
+ async def update(
110
+ self,
111
+ request: Request,
112
+ pk: UUID,
113
+ payload: dict[str, Any],
114
+ *,
115
+ partial: bool,
116
+ ) -> dict[str, Any]:
117
+ instance = await self.get_object(request, pk)
118
+ serializer = self.serializer(instance, partial=partial)
119
+ try:
120
+ await serializer.is_valid(payload)
121
+ except SerializerValidationError as exc:
122
+ raise HTTPException(
123
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
124
+ detail=exc.detail,
125
+ ) from exc
126
+ instance = await serializer.save()
127
+ return serializer.representation(instance)
128
+
129
+ async def destroy(self, request: Request, pk: UUID) -> None:
130
+ instance = await self.get_object(request, pk)
131
+ await instance.delete()
132
+
133
+ def as_router(self, *, prefix: str, tags: Sequence[str] | None = None) -> APIRouter:
134
+ """Build conventional collection/detail routes for this viewset."""
135
+ router = APIRouter(prefix=prefix, tags=[*tags] if tags else [])
136
+ input_schema = self.serializer_class.input_schema()
137
+ partial_input_schema = self.serializer_class._schema(for_input=True, partial=True)
138
+ output_schema = self.serializer_class.output_schema()
139
+ dependencies = [Depends(dependency) for dependency in self.authentication_dependencies]
140
+
141
+ @router.get("/", response_model=None, dependencies=dependencies)
142
+ async def list_view(request: Request) -> Any:
143
+ return await self.list(request)
144
+
145
+ @router.get("/{pk}", response_model=output_schema, dependencies=dependencies)
146
+ async def retrieve_view(request: Request, pk: UUID) -> Any:
147
+ return await self.retrieve(request, pk)
148
+
149
+ async def create_view(request: Request, payload: Any) -> Any:
150
+ return await self.create(request, payload.model_dump(exclude_unset=True))
151
+
152
+ async def update_view(request: Request, pk: UUID, payload: Any) -> Any:
153
+ return await self.update(
154
+ request, pk, payload.model_dump(exclude_unset=True), partial=False
155
+ )
156
+
157
+ async def partial_update_view(request: Request, pk: UUID, payload: Any) -> Any:
158
+ return await self.update(
159
+ request, pk, payload.model_dump(exclude_unset=True), partial=True
160
+ )
161
+
162
+ def signature(*parameters: inspect.Parameter) -> inspect.Signature:
163
+ return inspect.Signature(parameters)
164
+
165
+ request_parameter = inspect.Parameter(
166
+ "request", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Request
167
+ )
168
+ pk_parameter = inspect.Parameter(
169
+ "pk", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=UUID
170
+ )
171
+ cast(Any, create_view).__signature__ = signature(
172
+ request_parameter,
173
+ inspect.Parameter(
174
+ "payload",
175
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
176
+ default=Body(...),
177
+ annotation=input_schema,
178
+ ),
179
+ )
180
+ cast(Any, update_view).__signature__ = signature(
181
+ request_parameter,
182
+ pk_parameter,
183
+ inspect.Parameter(
184
+ "payload",
185
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
186
+ default=Body(...),
187
+ annotation=input_schema,
188
+ ),
189
+ )
190
+ cast(Any, partial_update_view).__signature__ = signature(
191
+ request_parameter,
192
+ pk_parameter,
193
+ inspect.Parameter(
194
+ "payload",
195
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
196
+ default=Body(...),
197
+ annotation=partial_input_schema,
198
+ ),
199
+ )
200
+ router.add_api_route(
201
+ "/",
202
+ create_view,
203
+ methods=["POST"],
204
+ response_model=output_schema,
205
+ status_code=status.HTTP_201_CREATED,
206
+ dependencies=dependencies,
207
+ )
208
+ router.add_api_route(
209
+ "/{pk}",
210
+ update_view,
211
+ methods=["PUT"],
212
+ response_model=output_schema,
213
+ dependencies=dependencies,
214
+ )
215
+ router.add_api_route(
216
+ "/{pk}",
217
+ partial_update_view,
218
+ methods=["PATCH"],
219
+ response_model=output_schema,
220
+ dependencies=dependencies,
221
+ )
222
+
223
+ @router.delete(
224
+ "/{pk}", status_code=status.HTTP_204_NO_CONTENT, dependencies=dependencies
225
+ )
226
+ async def destroy_view(request: Request, pk: UUID) -> None:
227
+ await self.destroy(request, pk)
228
+
229
+ return router
230
+