Skip to content

Commit

Permalink
👽️ ✨ document status codes in api server (ITISFoundation#5386)
Browse files Browse the repository at this point in the history
  • Loading branch information
bisgaard-itis authored Mar 5, 2024
1 parent 5f6cacf commit b4d276d
Show file tree
Hide file tree
Showing 15 changed files with 1,495 additions and 154 deletions.
1,285 changes: 1,252 additions & 33 deletions services/api-server/openapi.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
from typing import Any, Final

from fastapi import status
from typing import Final

from ...core.settings import BasicSettings

job_output_logfile_responses: dict[int | str, dict[str, Any]] = {
status.HTTP_200_OK: {
"content": {
"application/octet-stream": {
"schema": {"type": "string", "format": "binary"}
},
"application/zip": {"schema": {"type": "string", "format": "binary"}},
"text/plain": {"schema": {"type": "string"}},
},
"description": "Returns a log file",
},
status.HTTP_404_NOT_FOUND: {"description": "Log not found"},
}


API_SERVER_DEV_FEATURES_ENABLED: Final[
bool
] = BasicSettings.create_from_envs().API_SERVER_DEV_FEATURES_ENABLED
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import datetime
import io
import logging
from http import HTTPStatus
from textwrap import dedent
from typing import IO, Annotated, Final
from typing import IO, Annotated, Any
from uuid import UUID

from fastapi import APIRouter, Body, Depends
Expand All @@ -26,12 +27,15 @@
get_upload_links_from_s3,
)
from simcore_sdk.node_ports_common.filemanager import upload_path as storage_upload_path
from simcore_service_api_server.models.schemas.errors import ErrorGet
from simcore_service_api_server.services.service_exception_handling import (
DEFAULT_BACKEND_SERVICE_STATUS_CODES,
)
from starlette.datastructures import URL
from starlette.responses import RedirectResponse

from ..._meta import API_VTAG
from ...models.pagination import Page, PaginationParams
from ...models.schemas.errors import ErrorGet
from ...models.schemas.files import (
ClientFile,
ClientFileUploadData,
Expand Down Expand Up @@ -59,12 +63,12 @@
#
#

_COMMON_ERROR_RESPONSES: Final[dict] = {
_FILE_STATUS_CODES: dict[int | str, dict[str, Any]] = {
status.HTTP_404_NOT_FOUND: {
"description": "File not found",
"model": ErrorGet,
},
}
}
} | DEFAULT_BACKEND_SERVICE_STATUS_CODES


async def _get_file(
Expand Down Expand Up @@ -102,10 +106,7 @@ async def _get_file(
) from err


@router.get(
"",
response_model=list[File],
)
@router.get("", response_model=list[File], responses=_FILE_STATUS_CODES)
async def list_files(
storage_client: Annotated[StorageApi, Depends(get_api_client(StorageApi))],
user_id: Annotated[int, Depends(get_current_user_id)],
Expand Down Expand Up @@ -143,6 +144,8 @@ async def list_files(
"/page",
response_model=Page[File],
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
status_code=status.HTTP_501_NOT_IMPLEMENTED,
response_description=HTTPStatus(status.HTTP_501_NOT_IMPLEMENTED).description,
)
async def get_files_page(
storage_client: Annotated[StorageApi, Depends(get_api_client(StorageApi))],
Expand All @@ -163,10 +166,7 @@ def _get_spooled_file_size(file_io: IO) -> int:
return file_size


@router.put(
"/content",
response_model=File,
)
@router.put("/content", response_model=File, responses=_FILE_STATUS_CODES)
@cancel_on_disconnect
async def upload_file(
request: Request,
Expand Down Expand Up @@ -238,6 +238,7 @@ async def upload_files(files: list[UploadFile] = FileParam(...)):
"/content",
response_model=ClientFileUploadData,
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
responses=_FILE_STATUS_CODES,
)
@cancel_on_disconnect
async def get_upload_links(
Expand Down Expand Up @@ -279,7 +280,7 @@ async def get_upload_links(
@router.get(
"/{file_id}",
response_model=File,
responses={**_COMMON_ERROR_RESPONSES},
responses=_FILE_STATUS_CODES,
)
async def get_file(
file_id: UUID,
Expand All @@ -299,7 +300,7 @@ async def get_file(
@router.get(
":search",
response_model=Page[File],
responses={**_COMMON_ERROR_RESPONSES},
responses=_FILE_STATUS_CODES,
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
)
async def search_files_page(
Expand All @@ -310,38 +311,31 @@ async def search_files_page(
file_id: UUID | None = None,
):
"""Search files"""
try:
stored_files: list[StorageFileMetaData] = await storage_client.search_files(
user_id=user_id,
file_id=file_id,
sha256_checksum=sha256_checksum,
access_right="read",
)
error_message: str = "Not found in storage"
if page_params.offset > len(stored_files):
raise ValueError(error_message)
stored_files = stored_files[page_params.offset :]
if len(stored_files) > page_params.limit:
stored_files = stored_files[: page_params.limit]
return create_page(
[to_file_api_model(fmd) for fmd in stored_files],
len(stored_files),
page_params,
)

except (ValueError, ValidationError) as err:
_logger.debug(
"File with sha256_checksum=%d not found: %s", sha256_checksum, err
)
stored_files: list[StorageFileMetaData] = await storage_client.search_files(
user_id=user_id,
file_id=file_id,
sha256_checksum=sha256_checksum,
access_right="read",
)
if page_params.offset > len(stored_files):
_logger.debug("File with sha256_checksum=%d not found.", sha256_checksum)
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail=f"File with {sha256_checksum=} not found",
) from err
status_code=status.HTTP_404_NOT_FOUND, detail="Not found in storage"
)
stored_files = stored_files[page_params.offset :]
if len(stored_files) > page_params.limit:
stored_files = stored_files[: page_params.limit]
return create_page(
[to_file_api_model(fmd) for fmd in stored_files],
len(stored_files),
page_params,
)


@router.delete(
"/{file_id}",
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
responses=_FILE_STATUS_CODES,
)
async def delete_file(
file_id: UUID,
Expand All @@ -362,6 +356,7 @@ async def delete_file(
@router.post(
"/{file_id}:abort",
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
responses=DEFAULT_BACKEND_SERVICE_STATUS_CODES,
)
async def abort_multipart_upload(
request: Request,
Expand All @@ -388,6 +383,7 @@ async def abort_multipart_upload(
"/{file_id}:complete",
response_model=File,
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
responses=_FILE_STATUS_CODES,
)
@cancel_on_disconnect
async def complete_multipart_upload(
Expand Down Expand Up @@ -421,20 +417,7 @@ async def complete_multipart_upload(


@router.get(
"/{file_id}/content",
response_class=RedirectResponse,
responses={
**_COMMON_ERROR_RESPONSES,
200: {
"content": {
"application/octet-stream": {
"schema": {"type": "string", "format": "binary"}
},
"text/plain": {"schema": {"type": "string"}},
},
"description": "Returns a arbitrary binary data",
},
},
"/{file_id}/content", response_class=RedirectResponse, responses=_FILE_STATUS_CODES
)
async def download_file(
file_id: UUID,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import logging
from collections.abc import Callable
from operator import attrgetter
from typing import Annotated
from typing import Annotated, Any

from fastapi import APIRouter, Depends, HTTPException, status
from httpx import HTTPStatusError
from models_library.api_schemas_webserver.resource_usage import ServicePricingPlanGet
from pydantic import ValidationError
from pydantic.errors import PydanticValueError
from servicelib.error_codes import create_error_code
from simcore_service_api_server.models.schemas.errors import ErrorGet
from simcore_service_api_server.services.service_exception_handling import (
DEFAULT_BACKEND_SERVICE_STATUS_CODES,
)

from ...models.basic_types import VersionStr
from ...models.pagination import OnePage, Page, PaginationParams
Expand All @@ -22,6 +26,13 @@

_logger = logging.getLogger(__name__)

_SOLVER_STATUS_CODES: dict[int | str, dict[str, Any]] = {
status.HTTP_404_NOT_FOUND: {
"description": "Not found",
"model": ErrorGet,
}
} | DEFAULT_BACKEND_SERVICE_STATUS_CODES

router = APIRouter()

## SOLVERS -----------------------------------------------------------------------------------------
Expand All @@ -34,7 +45,7 @@
# Would be nice to have /solvers/foo/releases/latest or solvers/foo/releases/3 , similar to docker tagging


@router.get("", response_model=list[Solver])
@router.get("", response_model=list[Solver], responses=_SOLVER_STATUS_CODES)
async def list_solvers(
user_id: Annotated[int, Depends(get_current_user_id)],
catalog_client: Annotated[CatalogApi, Depends(get_api_client(CatalogApi))],
Expand All @@ -61,6 +72,8 @@ async def list_solvers(
"/page",
response_model=Page[Solver],
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
status_code=status.HTTP_501_NOT_IMPLEMENTED,
response_description="Not implemented",
)
async def get_solvers_page(
page_params: Annotated[PaginationParams, Depends()],
Expand All @@ -69,7 +82,12 @@ async def get_solvers_page(
raise NotImplementedError(msg)


@router.get("/releases", response_model=list[Solver], summary="Lists All Releases")
@router.get(
"/releases",
response_model=list[Solver],
summary="Lists All Releases",
responses=_SOLVER_STATUS_CODES,
)
async def list_solvers_releases(
user_id: Annotated[int, Depends(get_current_user_id)],
catalog_client: Annotated[CatalogApi, Depends(get_api_client(CatalogApi))],
Expand Down Expand Up @@ -98,6 +116,8 @@ async def list_solvers_releases(
"/releases/page",
response_model=Page[Solver],
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
status_code=status.HTTP_501_NOT_IMPLEMENTED,
response_description="Not implemented",
)
async def get_solvers_releases_page(
page_params: Annotated[PaginationParams, Depends()],
Expand All @@ -110,6 +130,7 @@ async def get_solvers_releases_page(
"/{solver_key:path}/latest",
response_model=Solver,
summary="Get Latest Release of a Solver",
responses=_SOLVER_STATUS_CODES,
)
async def get_solver(
solver_key: SolverKeyId,
Expand Down Expand Up @@ -138,7 +159,11 @@ async def get_solver(
) from err


@router.get("/{solver_key:path}/releases", response_model=list[Solver])
@router.get(
"/{solver_key:path}/releases",
response_model=list[Solver],
responses=_SOLVER_STATUS_CODES,
)
async def list_solver_releases(
solver_key: SolverKeyId,
user_id: Annotated[int, Depends(get_current_user_id)],
Expand Down Expand Up @@ -166,6 +191,8 @@ async def list_solver_releases(
"/{solver_key:path}/releases/page",
response_model=Page[Solver],
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
status_code=status.HTTP_501_NOT_IMPLEMENTED,
response_description="Not implemented",
)
async def get_solver_releases_page(
solver_key: SolverKeyId,
Expand All @@ -178,6 +205,7 @@ async def get_solver_releases_page(
@router.get(
"/{solver_key:path}/releases/{version}",
response_model=Solver,
responses=_SOLVER_STATUS_CODES,
)
async def get_solver_release(
solver_key: SolverKeyId,
Expand Down Expand Up @@ -217,6 +245,7 @@ async def get_solver_release(
@router.get(
"/{solver_key:path}/releases/{version}/ports",
response_model=OnePage[SolverPort],
responses=_SOLVER_STATUS_CODES,
)
async def list_solver_ports(
solver_key: SolverKeyId,
Expand Down Expand Up @@ -252,17 +281,12 @@ async def list_solver_ports(
detail=f"Port definition of {solver_key}:{version} seems corrupted [{error_code}]",
) from err

except HTTPStatusError as err:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ports for solver {solver_key}:{version} not found",
) from err


@router.get(
"/{solver_key:path}/releases/{version}/pricing_plan",
response_model=ServicePricingPlanGet,
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
responses=_SOLVER_STATUS_CODES,
)
async def get_solver_pricing_plan(
solver_key: SolverKeyId,
Expand Down
Loading

0 comments on commit b4d276d

Please sign in to comment.