Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions api/endpoints/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from api.core.filesystem import get_user_filesystem
from api.schemas.token import TokenResponse
from api.schemas.user import User, UserGroups
from api.schemas.common import ErrorResponse, MessageResponse
from api.settings import cognito_client_id, cognito_secret, cognito_user_pool_id

router = APIRouter()
Expand All @@ -22,7 +23,12 @@
"/user",
status_code=status.HTTP_201_CREATED,
response_model=User,
description="Register a new user",
description="Register a new user in the system",
responses={
201: {"description": "User successfully registered", "model": User},
400: {"description": "Invalid registration parameters or password requirements not met", "model": ErrorResponse},
409: {"description": "User already exists", "model": ErrorResponse}
}
)
def register_user(
user: OAuth2PasswordRequestForm = Depends(), groups: list[UserGroups] | None = None
Expand Down Expand Up @@ -73,7 +79,13 @@ def register_user(
"/token",
status_code=status.HTTP_201_CREATED,
response_model=TokenResponse,
description="Get a new login token",
description="Authenticate user and get a JWT token",
responses={
201: {"description": "Successfully authenticated", "model": TokenResponse},
400: {"description": "Invalid credentials", "model": ErrorResponse},
404: {"description": "User not found", "model": ErrorResponse},
500: {"description": "Internal server error", "model": ErrorResponse}
}
)
async def get_token(login: OAuth2PasswordRequestForm = Depends()) -> TokenResponse:
client = boto3.client("cognito-idp")
Expand Down Expand Up @@ -110,9 +122,16 @@ async def get_token(login: OAuth2PasswordRequestForm = Depends()) -> TokenRespon

@router.post(
"/login",
status_code=status.HTTP_201_CREATED,
status_code=status.HTTP_200_OK,
response_model=MessageResponse,
response_class=JSONResponse,
description="Login to the system (sets a cookie)",
description="Login to the system and set authentication cookie",
responses={
200: {"description": "Successfully logged in", "model": MessageResponse},
400: {"description": "Invalid credentials", "model": ErrorResponse},
404: {"description": "User not found", "model": ErrorResponse},
500: {"description": "Internal server error", "model": ErrorResponse}
}
)
def get_login(user: OAuth2PasswordRequestForm = Depends()) -> JSONResponse:
client = boto3.client("cognito-idp")
Expand Down
14 changes: 12 additions & 2 deletions api/endpoints/auth_get.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import enum

from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, status

from api.dependencies import GroupClaims, current_user_dep
from api.schemas.user import User
from api.schemas.common import ErrorResponse
from api.settings import cognito_client_id, cognito_region, cognito_user_pool_id

router = APIRouter()
Expand All @@ -16,7 +17,11 @@ class AccessType(enum.Enum):
@router.get(
"/access_info",
response_model=dict[AccessType, dict[str, str]],
status_code=status.HTTP_200_OK,
description="Get information about where API users should authenticate",
responses={
200: {"description": "Successfully retrieved access information", "model": dict[AccessType, dict[str, str]]}
}
)
def get_access_info() -> dict[AccessType, dict[str, str]]:
return {
Expand All @@ -31,7 +36,12 @@ def get_access_info() -> dict[AccessType, dict[str, str]]:
@router.get(
"/user",
response_model=User,
description="Get information about the authenticated user",
status_code=status.HTTP_200_OK,
description="Get information about the currently authenticated user",
responses={
200: {"description": "Successfully retrieved user information", "model": User},
401: {"description": "Authentication required", "model": ErrorResponse}
}
)
def describe_current_user(
current_user: GroupClaims = Depends(current_user_dep),
Expand Down
51 changes: 45 additions & 6 deletions api/endpoints/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from api.core.filesystem import FileSystem
from api.dependencies import filesystem_dep
from api.schemas import file as file_schemas
from api.schemas.common import ErrorResponse

router = APIRouter()

Expand All @@ -16,7 +17,12 @@
"/files/{file_path:path}/download",
response_model=None,
response_class=Response,
description="Download a file",
status_code=status.HTTP_200_OK,
description="Download a file from the file system",
responses={
200: {"description": "File successfully downloaded"},
404: {"description": "File not found", "model": ErrorResponse}
}
)
def download_file(
file_path: str, filesystem: FileSystem = Depends(filesystem_dep)
Expand All @@ -30,7 +36,12 @@ def download_file(
@router.get(
"/files/{file_path:path}/url",
response_model=file_schemas.FileHTTPRequest,
status_code=status.HTTP_200_OK,
description="Get request parameters (pre-signed URL) to download a file",
responses={
200: {"description": "Successfully generated download URL", "model": file_schemas.FileHTTPRequest},
404: {"description": "File not found", "model": ErrorResponse}
}
)
def get_download_presigned_url(
file_path: str, request: Request, filesystem: FileSystem = Depends(filesystem_dep)
Expand All @@ -46,7 +57,12 @@ def get_download_presigned_url(
@router.get(
"/files/{base_path:path}",
response_model=list[file_schemas.FileInfo],
description="List files in a directory",
status_code=status.HTTP_200_OK,
description="List files and directories in a specified path",
responses={
200: {"description": "Successfully retrieved file listing", "model": list[file_schemas.FileInfo]},
404: {"description": "Directory not found", "model": ErrorResponse}
}
)
def list_files(
base_path: str = "",
Expand All @@ -67,7 +83,11 @@ def list_files(
"/files/{f_type}/{base_path:path}/upload",
response_model=file_schemas.FileInfo,
status_code=status.HTTP_201_CREATED,
description="Upload a file",
description="Upload a file to the specified path",
responses={
201: {"description": "File successfully uploaded", "model": file_schemas.FileInfo},
400: {"description": "Invalid upload parameters", "model": ErrorResponse}
}
)
def upload_file(
f_type: models.UploadFileTypes,
Expand All @@ -86,6 +106,10 @@ def upload_file(
status_code=status.HTTP_201_CREATED,
response_model=file_schemas.FileHTTPRequest,
description="Get request parameters (pre-signed URL) to upload a file",
responses={
201: {"description": "Successfully generated upload URL", "model": file_schemas.FileHTTPRequest},
400: {"description": "Invalid parameters", "model": ErrorResponse}
}
)
def get_upload_presigned_url(
f_type: models.UploadFileTypes,
Expand All @@ -102,7 +126,12 @@ def get_upload_presigned_url(
@router.post(
"/files/{f_type}/{base_path:path}/",
status_code=status.HTTP_201_CREATED,
description="Create a directory",
response_model=None,
description="Create a new directory at the specified path",
responses={
201: {"description": "Directory successfully created"},
400: {"description": "Invalid directory parameters", "model": ErrorResponse}
}
)
def create_directory(
f_type: models.UploadFileTypes,
Expand All @@ -115,7 +144,13 @@ def create_directory(
@router.put(
"/files/{file_path:path}",
response_model=file_schemas.FileInfo,
description="Rename a file",
status_code=status.HTTP_200_OK,
description="Rename or move a file to a new path",
responses={
200: {"description": "File successfully renamed/moved", "model": file_schemas.FileInfo},
404: {"description": "File not found", "model": ErrorResponse},
405: {"description": "Operation not allowed (e.g., trying to rename directory)", "model": ErrorResponse}
}
)
def rename_file(
file_path: str,
Expand All @@ -138,7 +173,11 @@ def rename_file(
"/files/{file_path:path}",
status_code=status.HTTP_204_NO_CONTENT,
response_model=None,
description="Delete a file or directory",
description="Delete a file or directory and all its contents",
responses={
204: {"description": "File or directory successfully deleted"},
404: {"description": "File or directory not found", "model": ErrorResponse}
}
)
def delete_file(
file_path: str, filesystem: FileSystem = Depends(filesystem_dep)
Expand Down
6 changes: 6 additions & 0 deletions api/endpoints/job_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@
from api.dependencies import email_sender_dep, workerfacing_api_auth_dep
from api.models import JobStates
from api.schemas.job_update import JobUpdate
from api.schemas.common import ErrorResponse

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


@router.put(
"/_job_status",
response_model=JobStates,
status_code=status.HTTP_200_OK,
description="Internal endpoint for the worker-facing API to signal job status updates",
responses={
200: {"description": "Job status successfully updated", "model": JobStates},
404: {"description": "Job not found", "model": ErrorResponse}
}
)
def update_job_status(
update: JobUpdate,
Expand Down
38 changes: 34 additions & 4 deletions api/endpoints/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from api.crud import job as crud
from api.dependencies import enqueueing_function_dep
from api.schemas.job import Job, JobCreate, QueueJob
from api.schemas.common import ErrorResponse
from api.settings import application_config

router = APIRouter()
Expand All @@ -18,13 +19,25 @@
@router.get(
"/jobs/applications",
response_model=ApplicationConfig,
status_code=status.HTTP_200_OK,
description="List all available applications/versions/entrypoints",
responses={
200: {"description": "Successfully retrieved applications configuration", "model": ApplicationConfig}
}
)
def list_applications() -> ApplicationConfig:
return application_config.config


@router.get("/jobs", response_model=list[Job], description="List all jobs")
@router.get(
"/jobs",
response_model=list[Job],
status_code=status.HTTP_200_OK,
description="List all jobs for the authenticated user",
responses={
200: {"description": "Successfully retrieved list of jobs", "model": list[Job]}
}
)
def list_jobs(
request: Request,
offset: int = 0,
Expand All @@ -38,7 +51,16 @@ def list_jobs(
)


@router.get("/jobs/{job_id}", response_model=Job, description="Describe a job")
@router.get(
"/jobs/{job_id}",
response_model=Job,
status_code=status.HTTP_200_OK,
description="Get detailed information about a specific job",
responses={
200: {"description": "Successfully retrieved job details", "model": Job},
404: {"description": "Job not found", "model": ErrorResponse}
}
)
def describe_job(
request: Request, job_id: int, db: Session = Depends(database.get_db)
) -> Job:
Expand All @@ -54,7 +76,11 @@ def describe_job(
"/jobs",
status_code=status.HTTP_201_CREATED,
response_model=Job,
description="Start a job",
description="Create and start a new job",
responses={
201: {"description": "Job successfully created and started", "model": Job},
400: {"description": "Invalid job parameters or file not found", "model": ErrorResponse}
}
)
def start_job(
request: Request,
Expand All @@ -78,7 +104,11 @@ def start_job(
"/jobs/{job_id}",
status_code=status.HTTP_204_NO_CONTENT,
response_model=None,
description="Delete a job",
description="Delete a job and all associated data",
responses={
204: {"description": "Job successfully deleted"},
404: {"description": "Job not found", "model": ErrorResponse}
}
)
def delete_job(
request: Request, job_id: int, db: Session = Depends(database.get_db)
Expand Down
13 changes: 11 additions & 2 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

dotenv.load_dotenv()

from fastapi import Depends, FastAPI
from fastapi import Depends, FastAPI, status
from fastapi.middleware.cors import CORSMiddleware

from api import dependencies, settings, tags
from api.database import Base, engine
from api.endpoints import auth, auth_get, files, job_update, jobs
from api.exceptions import register_exception_handlers
from api.schemas.common import MessageResponse

app = FastAPI(openapi_tags=tags.tags_metadata)
if settings.frontend_url:
Expand Down Expand Up @@ -40,7 +41,15 @@
register_exception_handlers(app)


@app.get("/")
@app.get(
"/",
response_model=str,
status_code=status.HTTP_200_OK,
description="Welcome message for the DECODE OpenCloud User-facing API",
responses={
200: {"description": "Welcome message", "model": str}
}
)
async def root() -> str:
return "Welcome to the DECODE OpenCloud User-facing API"

Expand Down
23 changes: 23 additions & 0 deletions api/schemas/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from pydantic import BaseModel


class ErrorResponse(BaseModel):
detail: str

class Config:
schema_extra = {
"example": {
"detail": "Resource not found"
}
}


class MessageResponse(BaseModel):
message: str

class Config:
schema_extra = {
"example": {
"message": "Operation completed successfully"
}
}
21 changes: 21 additions & 0 deletions api/schemas/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,29 @@ class FileHTTPRequest(BaseModel):
headers: dict[str, Any] = {} # thank you pydantic, for handling mutable defaults
data: dict[str, Any] = {}

class Config:
schema_extra = {
"example": {
"method": "POST",
"url": "https://example.s3.amazonaws.com/uploads/myfile.txt",
"headers": {
"Content-Type": "application/octet-stream"
},
"data": {}
}
}


class FileInfo(BaseModel):
path: str
type: FileTypes
size: str

class Config:
schema_extra = {
"example": {
"path": "uploads/myfile.txt",
"type": "file",
"size": "1.2 MB"
}
}
Loading
Loading