From 1670fc0fc91afdd4f2c5468aaa295085bbc03473 Mon Sep 17 00:00:00 2001 From: Bruce Schultz Date: Mon, 11 Mar 2024 14:27:17 +0100 Subject: [PATCH] feat: add analysis and image hub eps with models --- gateway/core.py | 3 + gateway/models.py | 80 ++++++++++++++++++- gateway/routers/hub.py | 174 ++++++++++++++++++++++++++++++++++++++--- gateway/utils.py | 11 ++- 4 files changed, 253 insertions(+), 15 deletions(-) diff --git a/gateway/core.py b/gateway/core.py index 8d74370..9a7eb4d 100644 --- a/gateway/core.py +++ b/gateway/core.py @@ -49,6 +49,9 @@ async def make_request( query = {} async with httpx.AsyncClient(headers=headers) as client: + print(url) + print(query) + print(data) r = await client.request(url=url, method=method, params=query, data=data) resp_data = r.json() return resp_data, r.status_code diff --git a/gateway/models.py b/gateway/models.py index ccc8d70..7e84531 100644 --- a/gateway/models.py +++ b/gateway/models.py @@ -1,6 +1,7 @@ """Models for API.""" import datetime import uuid +from enum import Enum from typing import Optional from uuid import UUID @@ -169,18 +170,89 @@ class ImageDataResponse(BaseModel): # Hub Models -class ProjectResponse(BaseModel): - """Single project response model.""" +## String Models +class IncludeNode(BaseModel): + """Include node.""" + include: str = "node" + + +class ApprovalStatus(Enum): + """Status of project possibilities.""" + approved: str = "approved" + rejected: str = "rejected" + + +## Response Models +class BaseHubResponse(BaseModel): + """Common attributes of Hub responses.""" id: uuid.UUID - name: str - analyses: int created_at: datetime.datetime updated_at: datetime.datetime + + +class MasterImage(BaseHubResponse): + """Master image details.""" + path: str + virtual_path: str + group_virtual_path: str + name: str + command: str | None = None + command_arguments: str | None = None + + +class ProjectResponse(BaseHubResponse): + """Single project response model.""" + name: str + analyses: int realm_id: uuid.UUID user_id: uuid.UUID master_image_id: uuid.UUID | None = None + master_image: MasterImage | None = None class AllProjects(BaseModel): """List of all projects.""" data: list[ProjectResponse] + + +class NodeDetails(BaseHubResponse): + """Node details.""" + external_name: str | None = None + name: str + hidden: bool + type: str + online: bool + registry_id: uuid.UUID | None = None + registry_project_id: uuid.UUID | None = None + robot_id: uuid.UUID + realm_id: uuid.UUID + + +class AnalysisOrProjectNodeResponse(BaseHubResponse): + """Single project or analysis by node.""" + + approval_status: ApprovalStatus + comment: str | None = None + project_id: uuid.UUID | None = None + project_realm_id: uuid.UUID | None = None + node_id: uuid.UUID | None = None + node_realm_id: uuid.UUID | None = None + + +class ListAnalysisOrProjectNodeResponse(BaseModel): + data: list[AnalysisOrProjectNodeResponse] + + +class AnalysisNodeResponse(AnalysisOrProjectNodeResponse): + """Node analysis response model.""" + run_status: str | None = None + index: int + artifact_tag: str | None = None + artifact_digest: str | None = None + analysis_id: uuid.UUID + analysis_realm_id: uuid.UUID + node: NodeDetails | None = None + + +class ListAnalysisNodeResponse(BaseModel): + data: list[AnalysisNodeResponse] diff --git a/gateway/routers/hub.py b/gateway/routers/hub.py index e41ed6e..68eedac 100644 --- a/gateway/routers/hub.py +++ b/gateway/routers/hub.py @@ -1,7 +1,8 @@ """EPs for Hub provided information.""" import uuid +from typing import Annotated -from fastapi import APIRouter, Security +from fastapi import APIRouter, Security, Query, Body, Path from starlette import status from starlette.requests import Request from starlette.responses import Response @@ -9,10 +10,11 @@ from gateway.auth import hub_oauth2_scheme from gateway.conf import gateway_settings from gateway.core import route -from gateway.models import ImageDataResponse, ContainerResponse, ProjectResponse, AllProjects +from gateway.models import ImageDataResponse, ContainerResponse, ProjectResponse, AllProjects, \ + ApprovalStatus, AnalysisOrProjectNodeResponse, ListAnalysisNodeResponse, ListAnalysisOrProjectNodeResponse hub_router = APIRouter( - # dependencies=[Security(oauth2_scheme)], + dependencies=[Security(hub_oauth2_scheme)], tags=["Hub"], responses={404: {"description": "Not found"}}, ) @@ -91,13 +93,21 @@ async def get_vault_status(): status_code=status.HTTP_200_OK, service_url=gateway_settings.HUB_SERVICE_URL, response_model=AllProjects, - dependencies=[ - Security(hub_oauth2_scheme) # TODO: move to router definition - ] + query_params=["filter_id", "filter_realm_id", "filter_user_id", "include"], ) async def list_all_projects( request: Request, response: Response, + include: Annotated[ + str | None, + Query( + description="Whether to include additional data. Can only be 'master_image' or null", + pattern="^master_image$", # Must be "master_image", + ), + ] = None, + filter_id: Annotated[uuid.UUID, Query(description="Filter by object UUID.")] = None, + filter_realm_id: Annotated[uuid.UUID, Query(description="Filter by realm UUID.")] = None, + filter_user_id: Annotated[uuid.UUID, Query(description="Filter by user UUID.")] = None, ): """List all projects.""" pass @@ -109,14 +119,158 @@ async def list_all_projects( status_code=status.HTTP_200_OK, service_url=gateway_settings.HUB_SERVICE_URL, response_model=ProjectResponse, - dependencies=[ - Security(hub_oauth2_scheme) # TODO: move to router definition - ] ) async def list_specific_project( - project_id: uuid.UUID, + project_id: Annotated[uuid.UUID, Path(description="Project UUID.")], request: Request, response: Response, ): """List project for a given UUID.""" pass + + +@route( + request_method=hub_router.get, + path="/project-nodes", + status_code=status.HTTP_200_OK, + service_url=gateway_settings.HUB_SERVICE_URL, + response_model=ListAnalysisOrProjectNodeResponse, + query_params=["filter_id", "filter_approval_status", "filter_project_id", "filter_project_realm_id", + "filter_node_id", "filter_node_realm_id"], +) +async def list_project_node( + request: Request, + response: Response, + filter_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by ID of returned object.", + ), + ] = None, + filter_approval_status: Annotated[ + uuid.UUID | None, + Query( + description="Filter by approval status of project.", + ), + ] = None, + filter_project_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by project UUID.", + ), + ] = None, + filter_project_realm_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by project realm UUID.", + ), + ] = None, + filter_node_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by node UUID.", + ), + ] = None, + filter_node_realm_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by node realm UUID.", + ), + ] = None, +): + """List project for a node.""" + pass + + +@route( + request_method=hub_router.post, + path="/project-nodes", + status_code=status.HTTP_200_OK, + service_url=gateway_settings.HUB_SERVICE_URL, + response_model=AnalysisOrProjectNodeResponse, + body_params=["project_id", "node_id"], + query_params=["approval_status"], +) +async def create_project_node( + request: Request, + response: Response, + project_id: Annotated[uuid.UUID, Body(description="Project ID as UUID")], + node_id: Annotated[uuid.UUID, Body(description="Node ID as UUID")], + approval_status: Annotated[ApprovalStatus, Query( + description="Set the approval status of project for the node. Either 'rejected' or 'approved'" + )], +): + """Create a project at a specific node and set the approval status.""" + pass + + +@route( + request_method=hub_router.get, + path="/analysis-nodes", + status_code=status.HTTP_200_OK, + service_url=gateway_settings.HUB_SERVICE_URL, + response_model=ListAnalysisNodeResponse, + query_params=["filter_id", "filter_approval_status", "filter_project_id", "filter_project_realm_id", + "filter_node_id", "filter_node_realm_id", "include"], +) +async def list_analyses_of_node( + request: Request, + response: Response, + include: Annotated[ + str | None, + Query( + description="Whether to include additional data for the given parameter", + pattern="^node$", # Must be "node", + ), + ] = None, + filter_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by ID of returned object.", + ), + ] = None, + filter_approval_status: Annotated[ + uuid.UUID | None, + Query( + description="Filter by approval status of project.", + ), + ] = None, + filter_project_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by project UUID.", + ), + ] = None, + filter_project_realm_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by project realm UUID.", + ), + ] = None, + filter_node_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by node UUID.", + ), + ] = None, + filter_node_realm_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by node realm UUID.", + ), + ] = None, + filter_analysis_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by analysis UUID.", + ), + ] = None, + filter_analysis_realm_id: Annotated[ + uuid.UUID | None, + Query( + description="Filter by analysis realm UUID.", + ), + ] = None, +): + """List analyses for a node.""" + pass diff --git a/gateway/utils.py b/gateway/utils.py index 6852753..d492295 100644 --- a/gateway/utils.py +++ b/gateway/utils.py @@ -39,6 +39,14 @@ async def unzip_query_params( for key in necessary_params: value = all_params.get(key) + + if not value: # if value is None, then skip + continue + + if key.startswith("filter_"): # convert filter_some_param -> filter[some_param] for hub + filter_kw, filter_param = key.split("_", 1) + key = f"{filter_kw}[{filter_param}]" + serialized_dict = await serialize_query_content(key=key, value=value) response_query_params.update(serialized_dict) @@ -58,7 +66,8 @@ async def unzip_body_object( for key in specified_params: value = additional_params.get(key) _body_dict = await serialize_response(response_content=value) - response_body_dict.update(_body_dict) + # response_body_dict.update(_body_dict) + response_body_dict[key] = _body_dict return response_body_dict