Skip to content

Commit 5a01fae

Browse files
committed
SWP-302: added api schema
1 parent 0a7e3a6 commit 5a01fae

File tree

8 files changed

+71
-5
lines changed

8 files changed

+71
-5
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Django==4.1.13
55
django-elasticsearch-dsl==8.0
66
django-filter==2.4.0
77
djangorestframework==3.12.2
8+
drf-spectacular==0.29.0
89
elasticsearch<9
910
pikepdf==5.6.1
1011
playwright==1.27

swp/api/v1/publicationlist/viewsets.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from drf_spectacular.utils import extend_schema
12
from rest_framework.decorators import action
23

34
from swp.api.v1.viewsets import SWPViewSet
@@ -18,10 +19,12 @@ class PublicationListViewSet(SWPViewSet):
1819
def get_queryset(self):
1920
return self.queryset.filter(user=self.request.user)
2021

22+
@extend_schema(operation_id='publication_list_add_publication')
2123
@action(['POST'], detail=True, serializer_class=PublicationListAddSerializer)
2224
def add(self, request, *args, **kwargs):
2325
return self.update(request, *args, **kwargs)
2426

27+
@extend_schema(operation_id='publication_list_remove_publication')
2528
@action(['POST'], detail=True, serializer_class=PublicationListRemoveSerializer)
2629
def remove(self, request, *args, **kwargs):
2730
return self.update(request, *args, **kwargs)

swp/api/v1/scraper/serializers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from contextlib import suppress
2+
from typing import Optional
23

34
from django.utils.translation import gettext_lazy as _
45

@@ -12,6 +13,7 @@
1213
from swp.api.v1.serializers import ActivatableSerializer
1314
from swp.models import Scraper
1415
from swp.tasks import preview_scraper
16+
from swp.tasks.scraper import PreviewResult
1517

1618
STATES = [
1719
PENDING,
@@ -61,6 +63,6 @@ def update(self, instance: Scraper, validated_data):
6163
return preview_scraper.delay(start_url, data)
6264

6365
@staticmethod
64-
def get_result(instance: AsyncResult):
66+
def get_result(instance: AsyncResult) -> Optional[PreviewResult]:
6567
with suppress(TimeoutError):
6668
return instance.get(timeout=0.1, propagate=False)

swp/api/v1/scraper/viewsets.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.db.models import Prefetch
22

3+
from drf_spectacular.utils import extend_schema
34
from rest_framework.decorators import action
45
from rest_framework.response import Response
56

@@ -26,10 +27,12 @@ def check_object_permissions(self, request, obj: Scraper):
2627
if self.action in {'update', 'partial_update'} and obj.is_active:
2728
raise ScraperActiveException
2829

29-
@action(['POST'], detail=True, serializer_class=ScraperPreviewSerializer)
30+
@extend_schema(operation_id='scraper_preview_start')
31+
@action(['PATCH'], detail=True, serializer_class=ScraperPreviewSerializer)
3032
def preview(self, request, *args, **kwargs):
3133
return self.partial_update(request, *args, **kwargs)
3234

35+
@extend_schema(operation_id='scraper_preview_status')
3336
@action(detail=False, url_path=r'preview/(?P<task_id>[a-z0-9-]+)', serializer_class=ScraperPreviewSerializer)
3437
def preview_status(self, request, *, task_id: str, **kwargs):
3538
result = preview_scraper.AsyncResult(task_id)

swp/api/v1/spectacular.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import List
2+
3+
from drf_spectacular.openapi import AutoSchema
4+
5+
from .viewsets import SWPViewSet
6+
7+
8+
class SWPSchema(AutoSchema):
9+
10+
def is_excluded(self) -> bool:
11+
return 'v1' not in self.path
12+
13+
def _tokenize_path(self) -> List[str]:
14+
"""
15+
Remove the v1 prefix from tokenized path.
16+
"""
17+
18+
prefix, *tokenized_path = AutoSchema._tokenize_path(self)
19+
20+
return tokenized_path
21+
22+
def get_operation_id(self) -> str:
23+
operation_id = AutoSchema.get_operation_id(self)
24+
25+
if isinstance(self.view, SWPViewSet):
26+
if self.view.action not in self.method_mapping.values():
27+
action = self.method_mapping[self.method.lower()]
28+
29+
return operation_id.removesuffix(f'_{action}')
30+
31+
return operation_id

swp/settings/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
# Extensions
4444
'django_elasticsearch_dsl',
4545
'django_filters',
46+
'drf_spectacular',
4647
'rest_framework',
4748

4849
# Admin
@@ -263,6 +264,7 @@
263264
'rest_framework.permissions.IsAuthenticated',
264265
],
265266
'DEFAULT_RENDERER_CLASSES': REST_FRAMEWORK_DEFAULT_RENDERER_CLASSES,
267+
'DEFAULT_SCHEMA_CLASS': 'swp.api.v1.spectacular.SWPSchema',
266268
'DEFAULT_FILTER_BACKENDS': [
267269
'django_filters.rest_framework.DjangoFilterBackend',
268270
'rest_framework.filters.OrderingFilter',
@@ -273,6 +275,10 @@
273275

274276
# </editor-fold>
275277

278+
SPECTACULAR_SETTINGS = {
279+
'TITLE': 'WebMonitor',
280+
}
281+
276282
# <editor-fold desc="Zotero">
277283

278284
ZOTERO_API_BASE_URL = 'https://api.zotero.org'

swp/tasks/scraper.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import List, TypedDict, Union
2+
13
from asgiref.sync import async_to_sync
24

35
from swp.celery import app
@@ -9,6 +11,21 @@
911
PUBLICATION_PREVIEW_PAGES = 2
1012

1113

14+
class SuccessResult(TypedDict):
15+
success: bool
16+
publications: List[dict]
17+
max_per_page: int
18+
is_multipage: bool
19+
20+
21+
class ErrorResult(TypedDict):
22+
success: bool
23+
error: str
24+
25+
26+
PreviewResult = Union[SuccessResult, ErrorResult]
27+
28+
1229
def configure_preview_pagination(config: dict) -> int:
1330
""" Setup scraper config for limited pagination during preview. """
1431
paginator = config.pop('paginator', None) or {}
@@ -21,7 +38,7 @@ def configure_preview_pagination(config: dict) -> int:
2138
return max_pages * max_per_page
2239

2340

24-
async def scrape(scraper: Scraper, config: dict) -> dict:
41+
async def scrape(scraper: Scraper, config: dict) -> PreviewResult:
2542
publications = []
2643

2744
max_len = configure_preview_pagination(config)
@@ -57,15 +74,15 @@ async def scrape(scraper: Scraper, config: dict) -> dict:
5774
}
5875

5976

60-
def clean_publications(publications):
77+
def clean_publications(publications: List[dict]):
6178
for publication in publications:
6279
if fields := publication.get('fields'):
6380
for embedding_field in ['pdf_path', 'text_content']:
6481
fields.pop(embedding_field, None)
6582

6683

6784
@app.task(name='preview.scraper')
68-
def preview_scraper(start_url, config):
85+
def preview_scraper(start_url, config) -> PreviewResult:
6986
scraper = Scraper(start_url)
7087

7188
result = async_to_sync(scrape)(scraper, config)

swp/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from django.contrib import admin
33
from django.urls import include, path, register_converter
44

5+
from drf_spectacular.views import SpectacularAPIView
6+
57
from swp.api import default_router as internal
68
from swp.api.v1 import default_router as v1
79
from swp.converters import DateConverter
@@ -25,6 +27,7 @@
2527
path('admin/', admin.site.urls),
2628

2729
# api
30+
path('api/v1/schema/', SpectacularAPIView.as_view(), name='schema'),
2831
path('api/v1/', v1.urls),
2932
path('api/', internal.urls),
3033

0 commit comments

Comments
 (0)