Skip to content

Commit 9fa287b

Browse files
authored
Merge pull request #472 from amahuli03/460/generate-api-documentation
Generate API docs
2 parents 6ae14fe + 4bae746 commit 9fa287b

File tree

18 files changed

+301
-6
lines changed

18 files changed

+301
-6
lines changed

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,16 @@ Each module contains:
147147
- Auth endpoints via Djoser: `/auth/`
148148
- JWT token lifetime: 60 minutes (access), 1 day (refresh)
149149

150+
#### API Documentation
151+
- Auto-generated using **drf-spectacular** (OpenAPI 3.0)
152+
- **Swagger UI**: `http://localhost:8000/api/docs/` — interactive API explorer
153+
- **ReDoc**: `http://localhost:8000/api/redoc/` — readable reference docs
154+
- **Raw schema**: `http://localhost:8000/api/schema/`
155+
- Configuration in `SPECTACULAR_SETTINGS` in `settings.py`
156+
- Views use `@extend_schema` decorators and `serializer_class` attributes for schema generation
157+
- JWT auth is configured in the schema — use `JWT <token>` (not `Bearer`) in Swagger UI's Authorize dialog
158+
- To document a new endpoint: add `serializer_class` to the view if it has one, or add `@extend_schema` with `inline_serializer` for views returning raw dicts
159+
150160
#### Key Data Models
151161
- **Medication** (`api.views.listMeds.models`) - Medication catalog with benefits/risks
152162
- **MedRule** (`api.models.model_medRule`) - Include/Exclude rules for medications based on patient history

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,23 @@ df = pd.read_sql(query, engine)
7474
#### Django REST
7575
- The email and password are set in `server/api/management/commands/createsu.py`
7676

77+
## API Documentation
78+
79+
Interactive API docs are auto-generated using [drf-spectacular](https://drf-spectacular.readthedocs.io/) and available at:
80+
81+
- **Swagger UI**: [http://localhost:8000/api/docs/](http://localhost:8000/api/docs/) — interactive explorer with "Try it out" functionality
82+
- **ReDoc**: [http://localhost:8000/api/redoc/](http://localhost:8000/api/redoc/) — clean, readable reference docs
83+
- **Raw schema**: [http://localhost:8000/api/schema/](http://localhost:8000/api/schema/) — OpenAPI 3.0 JSON/YAML
84+
85+
### Testing authenticated endpoints
86+
87+
Most endpoints require JWT authentication. To test them in Swagger UI:
88+
89+
1. **Get a token**: Find the `POST /auth/jwt/create/` endpoint in Swagger UI, click **Try it out**, enter an authorized `email` and `password`, and click **Execute**. Copy the `access` token from the response.
90+
2. **Authorize**: Click the **Authorize** button (lock icon) at the top of the page. Enter `JWT <your-access-token>` in the value field. The prefix must be `JWT`, not `Bearer`.
91+
3. **Test endpoints**: All subsequent requests will include your token. Use **Try it out** on any protected endpoint.
92+
4. **Token refresh**: Access tokens expire after 60 minutes. Use `POST /auth/jwt/refresh/` with your `refresh` token, or repeat step 1.
93+
7794
## Architecture
7895

7996
The Balancer website is a Postgres, Django REST, and React project. The source code layout is:

server/api/views/ai_promptStorage/views.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from rest_framework import status
22
from rest_framework.decorators import api_view
33
from rest_framework.response import Response
4+
from drf_spectacular.utils import extend_schema
45
from .models import AI_PromptStorage
56
from .serializers import AI_PromptStorageSerializer
67

78

9+
@extend_schema(request=AI_PromptStorageSerializer, responses={201: AI_PromptStorageSerializer})
810
@api_view(['POST'])
911
# @permission_classes([IsAuthenticated])
1012
def store_prompt(request):
@@ -21,6 +23,7 @@ def store_prompt(request):
2123
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
2224

2325

26+
@extend_schema(responses={200: AI_PromptStorageSerializer(many=True)})
2427
@api_view(['GET'])
2528
def get_all_prompts(request):
2629
"""

server/api/views/ai_settings/views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
from rest_framework.decorators import api_view, permission_classes
33
from rest_framework.permissions import IsAuthenticated
44
from rest_framework.response import Response
5+
from drf_spectacular.utils import extend_schema
56
from .models import AI_Settings
67
from .serializers import AISettingsSerializer
78

89

10+
@extend_schema(request=AISettingsSerializer, responses={200: AISettingsSerializer(many=True), 201: AISettingsSerializer})
911
@api_view(['GET', 'POST'])
1012
@permission_classes([IsAuthenticated])
1113
def settings_view(request):

server/api/views/assistant/views.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from rest_framework.permissions import AllowAny
1111
from django.utils.decorators import method_decorator
1212
from django.views.decorators.csrf import csrf_exempt
13+
from drf_spectacular.utils import extend_schema, inline_serializer
14+
from rest_framework import serializers as drf_serializers
1315

1416
from openai import OpenAI
1517

@@ -113,6 +115,21 @@ def invoke_functions_from_response(
113115
class Assistant(APIView):
114116
permission_classes = [AllowAny]
115117

118+
@extend_schema(
119+
request=inline_serializer(name='AssistantRequest', fields={
120+
'message': drf_serializers.CharField(help_text='User message to send to the assistant'),
121+
'previous_response_id': drf_serializers.CharField(required=False, allow_null=True, help_text='ID of previous response for conversation continuity'),
122+
}),
123+
responses={
124+
200: inline_serializer(name='AssistantResponse', fields={
125+
'response_output_text': drf_serializers.CharField(),
126+
'final_response_id': drf_serializers.CharField(),
127+
}),
128+
500: inline_serializer(name='AssistantError', fields={
129+
'error': drf_serializers.CharField(),
130+
}),
131+
}
132+
)
116133
def post(self, request):
117134
try:
118135
user = request.user

server/api/views/conversations/views.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from .models import Conversation, Message
1717
from .serializers import ConversationSerializer
1818
from ...services.tools.tools import tools, execute_tool
19+
from drf_spectacular.utils import extend_schema, inline_serializer
20+
from rest_framework import serializers as drf_serializers
1921

2022

2123
@csrf_exempt
@@ -95,6 +97,21 @@ def destroy(self, request, *args, **kwargs):
9597
self.perform_destroy(instance)
9698
return Response(status=status.HTTP_204_NO_CONTENT)
9799

100+
@extend_schema(
101+
request=inline_serializer(name='ContinueConversationRequest', fields={
102+
'message': drf_serializers.CharField(help_text='User message to continue the conversation'),
103+
'page_context': drf_serializers.CharField(required=False, help_text='Optional page context'),
104+
}),
105+
responses={
106+
200: inline_serializer(name='ContinueConversationResponse', fields={
107+
'response': drf_serializers.CharField(),
108+
'title': drf_serializers.CharField(),
109+
}),
110+
400: inline_serializer(name='ContinueConversationBadRequest', fields={
111+
'error': drf_serializers.CharField(),
112+
}),
113+
}
114+
)
98115
@action(detail=True, methods=['post'])
99116
def continue_conversation(self, request, pk=None):
100117
conversation = self.get_object()
@@ -123,6 +140,20 @@ def continue_conversation(self, request, pk=None):
123140

124141
return Response({"response": chatgpt_response, "title": conversation.title})
125142

143+
@extend_schema(
144+
request=inline_serializer(name='UpdateTitleRequest', fields={
145+
'title': drf_serializers.CharField(help_text='New conversation title'),
146+
}),
147+
responses={
148+
200: inline_serializer(name='UpdateTitleResponse', fields={
149+
'status': drf_serializers.CharField(),
150+
'title': drf_serializers.CharField(),
151+
}),
152+
400: inline_serializer(name='UpdateTitleBadRequest', fields={
153+
'error': drf_serializers.CharField(),
154+
}),
155+
}
156+
)
126157
@action(detail=True, methods=['patch'])
127158
def update_title(self, request, pk=None):
128159
conversation = self.get_object()

server/api/views/embeddings/embeddingsView.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from rest_framework.views import APIView
22
from rest_framework.permissions import IsAuthenticated
33
from rest_framework.response import Response
4-
from rest_framework import status
4+
from rest_framework import status, serializers as drf_serializers
55
from django.http import StreamingHttpResponse
6+
from drf_spectacular.utils import extend_schema, inline_serializer, OpenApiParameter
67
from ...services.embedding_services import get_closest_embeddings
78
from ...services.conversions_services import convert_uuids
89
from ...services.openai_services import openAIServices
@@ -15,6 +16,26 @@
1516
class AskEmbeddingsAPIView(APIView):
1617
permission_classes = [IsAuthenticated]
1718

19+
@extend_schema(
20+
parameters=[
21+
OpenApiParameter(name='guid', type=str, location=OpenApiParameter.QUERY, required=False, description='Optional file GUID to filter embeddings'),
22+
OpenApiParameter(name='stream', type=bool, location=OpenApiParameter.QUERY, required=False, description='Enable streaming response'),
23+
],
24+
request=inline_serializer(name='AskEmbeddingsRequest', fields={
25+
'message': drf_serializers.CharField(help_text='Question to ask against embedded documents'),
26+
}),
27+
responses={
28+
200: inline_serializer(name='AskEmbeddingsResponse', fields={
29+
'question': drf_serializers.CharField(),
30+
'llm_response': drf_serializers.CharField(),
31+
'embeddings_info': drf_serializers.CharField(),
32+
'sent_to_llm': drf_serializers.CharField(),
33+
}),
34+
400: inline_serializer(name='AskEmbeddingsBadRequest', fields={
35+
'error': drf_serializers.CharField(),
36+
}),
37+
}
38+
)
1839
def post(self, request, *args, **kwargs):
1940
try:
2041
user = request.user

server/api/views/feedback/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
class FeedbackView(APIView):
1111
permission_classes = [AllowAny]
12+
serializer_class = FeedbackSerializer
1213

1314
def post(self, request, *args, **kwargs):
1415
serializer = FeedbackSerializer(data=request.data)

server/api/views/listMeds/views.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from rest_framework import status
1+
from rest_framework import status, serializers as drf_serializers
22
from rest_framework.permissions import AllowAny
33
from rest_framework.response import Response
44
from rest_framework.views import APIView
5+
from drf_spectacular.utils import extend_schema, inline_serializer
56

67
from .models import Diagnosis, Medication, Suggestion
78
from .serializers import MedicationSerializer
@@ -24,6 +25,33 @@
2425
class GetMedication(APIView):
2526
permission_classes = [AllowAny]
2627

28+
@extend_schema(
29+
request=inline_serializer(
30+
name='GetMedicationRequest',
31+
fields={
32+
'state': drf_serializers.CharField(help_text='Diagnosis state, e.g. "depressed", "manic"'),
33+
'suicideHistory': drf_serializers.BooleanField(default=False),
34+
'kidneyHistory': drf_serializers.BooleanField(default=False),
35+
'liverHistory': drf_serializers.BooleanField(default=False),
36+
'bloodPressureHistory': drf_serializers.BooleanField(default=False),
37+
'weightGainConcern': drf_serializers.BooleanField(default=False),
38+
'priorMedications': drf_serializers.CharField(required=False, default='', help_text='Comma-separated medication names'),
39+
}
40+
),
41+
responses={
42+
200: inline_serializer(
43+
name='GetMedicationResponse',
44+
fields={
45+
'first': drf_serializers.ListField(child=drf_serializers.DictField()),
46+
'second': drf_serializers.ListField(child=drf_serializers.DictField()),
47+
'third': drf_serializers.ListField(child=drf_serializers.DictField()),
48+
}
49+
),
50+
404: inline_serializer(name='GetMedicationNotFound', fields={
51+
'error': drf_serializers.CharField(),
52+
}),
53+
}
54+
)
2755
def post(self, request):
2856
data = request.data
2957
state_query = data.get('state', '')
@@ -75,6 +103,7 @@ def post(self, request):
75103

76104
class ListOrDetailMedication(APIView):
77105
permission_classes = [AllowAny]
106+
serializer_class = MedicationSerializer
78107

79108
def get(self, request):
80109
name_query = request.query_params.get('name', None)
@@ -98,6 +127,7 @@ class AddMedication(APIView):
98127
"""
99128
API endpoint to add a medication to the database with its risks and benefits.
100129
"""
130+
serializer_class = MedicationSerializer
101131

102132
def post(self, request):
103133
data = request.data
@@ -129,6 +159,22 @@ class DeleteMedication(APIView):
129159
API endpoint to delete medication if medication in database.
130160
"""
131161

162+
@extend_schema(
163+
request=inline_serializer(name='DeleteMedicationRequest', fields={
164+
'name': drf_serializers.CharField(),
165+
}),
166+
responses={
167+
200: inline_serializer(name='DeleteMedicationSuccess', fields={
168+
'success': drf_serializers.CharField(),
169+
}),
170+
400: inline_serializer(name='DeleteMedicationBadRequest', fields={
171+
'error': drf_serializers.CharField(),
172+
}),
173+
404: inline_serializer(name='DeleteMedicationNotFound', fields={
174+
'error': drf_serializers.CharField(),
175+
}),
176+
}
177+
)
132178
def delete(self, request):
133179
data = request.data
134180
name = data.get('name', '').strip()

server/api/views/medRules/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from rest_framework import serializers
2+
from drf_spectacular.utils import extend_schema_field
23
from ...models.model_medRule import MedRule, MedRuleSource
34
from ..listMeds.serializers import MedicationSerializer
45
from ...models.model_embeddings import Embeddings
@@ -30,6 +31,7 @@ class Meta:
3031
"medication_sources",
3132
]
3233

34+
@extend_schema_field(MedicationWithSourcesSerializer(many=True))
3335
def get_medication_sources(self, obj):
3436
medrule_sources = MedRuleSource.objects.filter(medrule=obj).select_related(
3537
"medication", "embedding"

0 commit comments

Comments
 (0)