Skip to content

Commit 00f3bbe

Browse files
Copilotnolan1999
andcommitted
Add comprehensive OpenAPI specifications to all FastAPI endpoints
Co-authored-by: nolan1999 <54246789+nolan1999@users.noreply.github.com>
1 parent a24ab02 commit 00f3bbe

11 files changed

Lines changed: 232 additions & 18 deletions

File tree

api/endpoints/auth.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from api.core.filesystem import get_user_filesystem
1414
from api.schemas.token import TokenResponse
1515
from api.schemas.user import User, UserGroups
16+
from api.schemas.common import ErrorResponse, MessageResponse
1617
from api.settings import cognito_client_id, cognito_secret, cognito_user_pool_id
1718

1819
router = APIRouter()
@@ -22,7 +23,12 @@
2223
"/user",
2324
status_code=status.HTTP_201_CREATED,
2425
response_model=User,
25-
description="Register a new user",
26+
description="Register a new user in the system",
27+
responses={
28+
201: {"description": "User successfully registered", "model": User},
29+
400: {"description": "Invalid registration parameters or password requirements not met", "model": ErrorResponse},
30+
409: {"description": "User already exists", "model": ErrorResponse}
31+
}
2632
)
2733
def register_user(
2834
user: OAuth2PasswordRequestForm = Depends(), groups: list[UserGroups] | None = None
@@ -73,7 +79,13 @@ def register_user(
7379
"/token",
7480
status_code=status.HTTP_201_CREATED,
7581
response_model=TokenResponse,
76-
description="Get a new login token",
82+
description="Authenticate user and get a JWT token",
83+
responses={
84+
201: {"description": "Successfully authenticated", "model": TokenResponse},
85+
400: {"description": "Invalid credentials", "model": ErrorResponse},
86+
404: {"description": "User not found", "model": ErrorResponse},
87+
500: {"description": "Internal server error", "model": ErrorResponse}
88+
}
7789
)
7890
async def get_token(login: OAuth2PasswordRequestForm = Depends()) -> TokenResponse:
7991
client = boto3.client("cognito-idp")
@@ -110,9 +122,16 @@ async def get_token(login: OAuth2PasswordRequestForm = Depends()) -> TokenRespon
110122

111123
@router.post(
112124
"/login",
113-
status_code=status.HTTP_201_CREATED,
125+
status_code=status.HTTP_200_OK,
126+
response_model=MessageResponse,
114127
response_class=JSONResponse,
115-
description="Login to the system (sets a cookie)",
128+
description="Login to the system and set authentication cookie",
129+
responses={
130+
200: {"description": "Successfully logged in", "model": MessageResponse},
131+
400: {"description": "Invalid credentials", "model": ErrorResponse},
132+
404: {"description": "User not found", "model": ErrorResponse},
133+
500: {"description": "Internal server error", "model": ErrorResponse}
134+
}
116135
)
117136
def get_login(user: OAuth2PasswordRequestForm = Depends()) -> JSONResponse:
118137
client = boto3.client("cognito-idp")

api/endpoints/auth_get.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import enum
22

3-
from fastapi import APIRouter, Depends
3+
from fastapi import APIRouter, Depends, status
44

55
from api.dependencies import GroupClaims, current_user_dep
66
from api.schemas.user import User
7+
from api.schemas.common import ErrorResponse
78
from api.settings import cognito_client_id, cognito_region, cognito_user_pool_id
89

910
router = APIRouter()
@@ -16,7 +17,11 @@ class AccessType(enum.Enum):
1617
@router.get(
1718
"/access_info",
1819
response_model=dict[AccessType, dict[str, str]],
20+
status_code=status.HTTP_200_OK,
1921
description="Get information about where API users should authenticate",
22+
responses={
23+
200: {"description": "Successfully retrieved access information", "model": dict[AccessType, dict[str, str]]}
24+
}
2025
)
2126
def get_access_info() -> dict[AccessType, dict[str, str]]:
2227
return {
@@ -31,7 +36,12 @@ def get_access_info() -> dict[AccessType, dict[str, str]]:
3136
@router.get(
3237
"/user",
3338
response_model=User,
34-
description="Get information about the authenticated user",
39+
status_code=status.HTTP_200_OK,
40+
description="Get information about the currently authenticated user",
41+
responses={
42+
200: {"description": "Successfully retrieved user information", "model": User},
43+
401: {"description": "Authentication required", "model": ErrorResponse}
44+
}
3545
)
3646
def describe_current_user(
3747
current_user: GroupClaims = Depends(current_user_dep),

api/endpoints/files.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from api.core.filesystem import FileSystem
99
from api.dependencies import filesystem_dep
1010
from api.schemas import file as file_schemas
11+
from api.schemas.common import ErrorResponse
1112

1213
router = APIRouter()
1314

@@ -16,7 +17,12 @@
1617
"/files/{file_path:path}/download",
1718
response_model=None,
1819
response_class=Response,
19-
description="Download a file",
20+
status_code=status.HTTP_200_OK,
21+
description="Download a file from the file system",
22+
responses={
23+
200: {"description": "File successfully downloaded"},
24+
404: {"description": "File not found", "model": ErrorResponse}
25+
}
2026
)
2127
def download_file(
2228
file_path: str, filesystem: FileSystem = Depends(filesystem_dep)
@@ -30,7 +36,12 @@ def download_file(
3036
@router.get(
3137
"/files/{file_path:path}/url",
3238
response_model=file_schemas.FileHTTPRequest,
39+
status_code=status.HTTP_200_OK,
3340
description="Get request parameters (pre-signed URL) to download a file",
41+
responses={
42+
200: {"description": "Successfully generated download URL", "model": file_schemas.FileHTTPRequest},
43+
404: {"description": "File not found", "model": ErrorResponse}
44+
}
3445
)
3546
def get_download_presigned_url(
3647
file_path: str, request: Request, filesystem: FileSystem = Depends(filesystem_dep)
@@ -46,7 +57,12 @@ def get_download_presigned_url(
4657
@router.get(
4758
"/files/{base_path:path}",
4859
response_model=list[file_schemas.FileInfo],
49-
description="List files in a directory",
60+
status_code=status.HTTP_200_OK,
61+
description="List files and directories in a specified path",
62+
responses={
63+
200: {"description": "Successfully retrieved file listing", "model": list[file_schemas.FileInfo]},
64+
404: {"description": "Directory not found", "model": ErrorResponse}
65+
}
5066
)
5167
def list_files(
5268
base_path: str = "",
@@ -67,7 +83,11 @@ def list_files(
6783
"/files/{f_type}/{base_path:path}/upload",
6884
response_model=file_schemas.FileInfo,
6985
status_code=status.HTTP_201_CREATED,
70-
description="Upload a file",
86+
description="Upload a file to the specified path",
87+
responses={
88+
201: {"description": "File successfully uploaded", "model": file_schemas.FileInfo},
89+
400: {"description": "Invalid upload parameters", "model": ErrorResponse}
90+
}
7191
)
7292
def upload_file(
7393
f_type: models.UploadFileTypes,
@@ -86,6 +106,10 @@ def upload_file(
86106
status_code=status.HTTP_201_CREATED,
87107
response_model=file_schemas.FileHTTPRequest,
88108
description="Get request parameters (pre-signed URL) to upload a file",
109+
responses={
110+
201: {"description": "Successfully generated upload URL", "model": file_schemas.FileHTTPRequest},
111+
400: {"description": "Invalid parameters", "model": ErrorResponse}
112+
}
89113
)
90114
def get_upload_presigned_url(
91115
f_type: models.UploadFileTypes,
@@ -102,7 +126,12 @@ def get_upload_presigned_url(
102126
@router.post(
103127
"/files/{f_type}/{base_path:path}/",
104128
status_code=status.HTTP_201_CREATED,
105-
description="Create a directory",
129+
response_model=None,
130+
description="Create a new directory at the specified path",
131+
responses={
132+
201: {"description": "Directory successfully created"},
133+
400: {"description": "Invalid directory parameters", "model": ErrorResponse}
134+
}
106135
)
107136
def create_directory(
108137
f_type: models.UploadFileTypes,
@@ -115,7 +144,13 @@ def create_directory(
115144
@router.put(
116145
"/files/{file_path:path}",
117146
response_model=file_schemas.FileInfo,
118-
description="Rename a file",
147+
status_code=status.HTTP_200_OK,
148+
description="Rename or move a file to a new path",
149+
responses={
150+
200: {"description": "File successfully renamed/moved", "model": file_schemas.FileInfo},
151+
404: {"description": "File not found", "model": ErrorResponse},
152+
405: {"description": "Operation not allowed (e.g., trying to rename directory)", "model": ErrorResponse}
153+
}
119154
)
120155
def rename_file(
121156
file_path: str,
@@ -138,7 +173,11 @@ def rename_file(
138173
"/files/{file_path:path}",
139174
status_code=status.HTTP_204_NO_CONTENT,
140175
response_model=None,
141-
description="Delete a file or directory",
176+
description="Delete a file or directory and all its contents",
177+
responses={
178+
204: {"description": "File or directory successfully deleted"},
179+
404: {"description": "File or directory not found", "model": ErrorResponse}
180+
}
142181
)
143182
def delete_file(
144183
file_path: str, filesystem: FileSystem = Depends(filesystem_dep)

api/endpoints/job_update.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,20 @@
99
from api.dependencies import email_sender_dep, workerfacing_api_auth_dep
1010
from api.models import JobStates
1111
from api.schemas.job_update import JobUpdate
12+
from api.schemas.common import ErrorResponse
1213

1314
router = APIRouter(dependencies=[Depends(workerfacing_api_auth_dep)])
1415

1516

1617
@router.put(
1718
"/_job_status",
1819
response_model=JobStates,
20+
status_code=status.HTTP_200_OK,
1921
description="Internal endpoint for the worker-facing API to signal job status updates",
22+
responses={
23+
200: {"description": "Job status successfully updated", "model": JobStates},
24+
404: {"description": "Job not found", "model": ErrorResponse}
25+
}
2026
)
2127
def update_job_status(
2228
update: JobUpdate,

api/endpoints/jobs.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from api.crud import job as crud
88
from api.dependencies import enqueueing_function_dep
99
from api.schemas.job import Job, JobCreate, QueueJob
10+
from api.schemas.common import ErrorResponse
1011
from api.settings import application_config
1112

1213
router = APIRouter()
@@ -18,13 +19,25 @@
1819
@router.get(
1920
"/jobs/applications",
2021
response_model=ApplicationConfig,
22+
status_code=status.HTTP_200_OK,
2123
description="List all available applications/versions/entrypoints",
24+
responses={
25+
200: {"description": "Successfully retrieved applications configuration", "model": ApplicationConfig}
26+
}
2227
)
2328
def list_applications() -> ApplicationConfig:
2429
return application_config.config
2530

2631

27-
@router.get("/jobs", response_model=list[Job], description="List all jobs")
32+
@router.get(
33+
"/jobs",
34+
response_model=list[Job],
35+
status_code=status.HTTP_200_OK,
36+
description="List all jobs for the authenticated user",
37+
responses={
38+
200: {"description": "Successfully retrieved list of jobs", "model": list[Job]}
39+
}
40+
)
2841
def list_jobs(
2942
request: Request,
3043
offset: int = 0,
@@ -38,7 +51,16 @@ def list_jobs(
3851
)
3952

4053

41-
@router.get("/jobs/{job_id}", response_model=Job, description="Describe a job")
54+
@router.get(
55+
"/jobs/{job_id}",
56+
response_model=Job,
57+
status_code=status.HTTP_200_OK,
58+
description="Get detailed information about a specific job",
59+
responses={
60+
200: {"description": "Successfully retrieved job details", "model": Job},
61+
404: {"description": "Job not found", "model": ErrorResponse}
62+
}
63+
)
4264
def describe_job(
4365
request: Request, job_id: int, db: Session = Depends(database.get_db)
4466
) -> Job:
@@ -54,7 +76,11 @@ def describe_job(
5476
"/jobs",
5577
status_code=status.HTTP_201_CREATED,
5678
response_model=Job,
57-
description="Start a job",
79+
description="Create and start a new job",
80+
responses={
81+
201: {"description": "Job successfully created and started", "model": Job},
82+
400: {"description": "Invalid job parameters or file not found", "model": ErrorResponse}
83+
}
5884
)
5985
def start_job(
6086
request: Request,
@@ -78,7 +104,11 @@ def start_job(
78104
"/jobs/{job_id}",
79105
status_code=status.HTTP_204_NO_CONTENT,
80106
response_model=None,
81-
description="Delete a job",
107+
description="Delete a job and all associated data",
108+
responses={
109+
204: {"description": "Job successfully deleted"},
110+
404: {"description": "Job not found", "model": ErrorResponse}
111+
}
82112
)
83113
def delete_job(
84114
request: Request, job_id: int, db: Session = Depends(database.get_db)

api/main.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
dotenv.load_dotenv()
44

5-
from fastapi import Depends, FastAPI
5+
from fastapi import Depends, FastAPI, status
66
from fastapi.middleware.cors import CORSMiddleware
77

88
from api import dependencies, settings, tags
99
from api.database import Base, engine
1010
from api.endpoints import auth, auth_get, files, job_update, jobs
1111
from api.exceptions import register_exception_handlers
12+
from api.schemas.common import MessageResponse
1213

1314
app = FastAPI(openapi_tags=tags.tags_metadata)
1415
if settings.frontend_url:
@@ -40,7 +41,15 @@
4041
register_exception_handlers(app)
4142

4243

43-
@app.get("/")
44+
@app.get(
45+
"/",
46+
response_model=str,
47+
status_code=status.HTTP_200_OK,
48+
description="Welcome message for the DECODE OpenCloud User-facing API",
49+
responses={
50+
200: {"description": "Welcome message", "model": str}
51+
}
52+
)
4453
async def root() -> str:
4554
return "Welcome to the DECODE OpenCloud User-facing API"
4655

api/schemas/common.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from pydantic import BaseModel
2+
3+
4+
class ErrorResponse(BaseModel):
5+
detail: str
6+
7+
class Config:
8+
schema_extra = {
9+
"example": {
10+
"detail": "Resource not found"
11+
}
12+
}
13+
14+
15+
class MessageResponse(BaseModel):
16+
message: str
17+
18+
class Config:
19+
schema_extra = {
20+
"example": {
21+
"message": "Operation completed successfully"
22+
}
23+
}

api/schemas/file.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,29 @@ class FileHTTPRequest(BaseModel):
3131
headers: dict[str, Any] = {} # thank you pydantic, for handling mutable defaults
3232
data: dict[str, Any] = {}
3333

34+
class Config:
35+
schema_extra = {
36+
"example": {
37+
"method": "POST",
38+
"url": "https://example.s3.amazonaws.com/uploads/myfile.txt",
39+
"headers": {
40+
"Content-Type": "application/octet-stream"
41+
},
42+
"data": {}
43+
}
44+
}
45+
3446

3547
class FileInfo(BaseModel):
3648
path: str
3749
type: FileTypes
3850
size: str
51+
52+
class Config:
53+
schema_extra = {
54+
"example": {
55+
"path": "uploads/myfile.txt",
56+
"type": "file",
57+
"size": "1.2 MB"
58+
}
59+
}

0 commit comments

Comments
 (0)