diff --git a/services/capability/capability/routes.py b/services/capability/capability/routes.py index ce095dc657f9e42d791cf948960f8b96cffbe37e..a2ad6c15cba34c1f06cba35315d49b3ed2d992ce 100644 --- a/services/capability/capability/routes.py +++ b/services/capability/capability/routes.py @@ -59,18 +59,15 @@ def capability_request_routes(config: Configurator): # GET config.add_route("view_capability_request", f"{request_url}", request_method="GET") # POST - # TODO: Make it so {capability_name} in URL can be passed to capability request config.add_route( "create_capability_request", "capability/{capability_name}/request/create", request_method="POST", - request_param=["parameters", "versions"], ) config.add_route( "edit_capability_request", f"{request_url}/edit", request_method="POST", - request_param=["parameters", "versions"], ) config.add_route("submit_capability_request", f"{request_url}/submit", request_method="POST") config.add_route("cancel_capability_request", f"{request_url}/cancel", request_method="POST") diff --git a/services/capability/capability/views/capability.py b/services/capability/capability/views/capability.py index 14638b4993b9df0e7befba0684b89a192f1e895d..9950b2e2b30eb7f9092c5189cc6879bf6f3508cd 100644 --- a/services/capability/capability/views/capability.py +++ b/services/capability/capability/views/capability.py @@ -26,7 +26,8 @@ def view_capability(request: Request) -> Response: URL: capability/{capability_name} :param request: GET request - :return: Response with JSON-formatted capability info if found or 404 response (HTTPNotFound) + :return: Response with JSON-formatted capability info if found + or 404 response (HTTPNotFound) """ capability = request.capability_info.lookup_capability(request.matchdict["capability_name"]) if capability: @@ -44,8 +45,8 @@ def create_capability(request: Request) -> Response: :param request: POST request, expecting JSON parameters ["capability_name", "steps", "max_jobs"] optionally, accepts a boolean "enabled" parameter :return: Response with JSON-formatted capability info of newly created capability - or 400 response (HTTPBadRequest) if expected parameters not given - or 412 response (HTTPPreconditionFailed) if capability with given name already exists + or 400 response (HTTPBadRequest) if expected parameters not given + or 412 response (HTTPPreconditionFailed) if capability with given name already exists """ expected_params = ["capability_name", "steps", "max_jobs"] params = request.json_body @@ -82,8 +83,9 @@ def edit_capability(request: Request) -> Response: :param request: POST request, expecting JSON parameters ["capability_name", "steps", "max_jobs"] :return: Response with JSON-formatted capability info of newly created capability - or 400 response (HTTPBadRequest) if expected parameters not given - or 412 response (HTTPPreconditionFailed) if capability with given name does not exist + or 400 response (HTTPBadRequest) if expected parameters not given + or 412 response (HTTPPreconditionFailed) if capability with given name does not exist + or 417 response (HTTPExpectationFailed) if the capability was unable to be edited TODO: In the future, we should check if there are any active requests for the capability being edited and disallow its editing until they are finished @@ -117,6 +119,14 @@ def edit_capability(request: Request) -> Response: @view_config(route_name="enable_capability", renderer="json") def enable_capability(request: Request) -> Response: + """ + Pyramid view that enables a capability + + :param request: POST request + :return: HTTP 200 response + or 412 response (HTTPPreconditionFailed) if capability with given name does not exist + or 417 response (HTTPExpectationFailed) if the capability was unable to be enabled + """ capability_name = request.matchdict["capability_name"] if not request.capability_info.lookup_capability(capability_name): @@ -133,6 +143,14 @@ def enable_capability(request: Request) -> Response: @view_config(route_name="disable_capability", renderer="json") def disable_capability(request: Request) -> Response: + """ + Pyramid view that disables a capability + + :param request: POST request + :return: HTTP 200 response + or 412 response (HTTPPreconditionFailed) if capability with given name does not exist + or 417 response (HTTPExpectationFailed) if the capability was unable to be disabled + """ capability_name = request.matchdict["capability_name"] if not request.capability_info.lookup_capability(capability_name): diff --git a/services/capability/capability/views/capability_request.py b/services/capability/capability/views/capability_request.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..78f5636d82d7b88943d2c9f647dff67c9cce6d77 100644 --- a/services/capability/capability/views/capability_request.py +++ b/services/capability/capability/views/capability_request.py @@ -0,0 +1,92 @@ +""" +.. codeauthor:: Nathan Hertz <nhertz@nrao.edu> + +File containing definitions for the other half of the capability side of the Workspaces REST API, +concerning capability requests +""" +from pyramid.httpexceptions import ( + HTTPBadRequest, + HTTPExpectationFailed, + HTTPNotFound, + HTTPPreconditionFailed, +) +from pyramid.request import Request +from pyramid.response import Response +from pyramid.view import view_config + +from workspaces.capability.schema import CapabilityRequest + + +@view_config(route_name="view_capability_request", renderer="json") +def view_capability_request(request: Request) -> Response: + """ + Pyramid view that accepts a request to view a capability request and responds with the request's info, if it exists + URL: capability/{capability_name}/request/{request_id} + + :param request: GET request + :return: Response with JSON-formatted capability request info if found + or 404 response (HTTPNotFound) + """ + capability_request = request.capability_info.lookup_capability_request( + request.matchdict["request_id"] + ) + if capability_request: + return Response(json_body=capability_request.__json__()) + else: + not_found_msg = ( + f"Capability request for capability {request.matchdict['capability_name']}", + f"with ID {request.matchdict['request_id']} not found.", + ) + return HTTPNotFound(detail=not_found_msg) + + +@view_config(route_name="create_capability_request", renderer="json") +def create_capability_request(request: Request) -> Response: + """ + Pyramid view that accepts a request to create a capability request + URL: capability/{capability_name}/request/create + + :param request: POST request, expecting JSON parameters ["parameters", "versions"] + :return: Response with JSON-formatted info of newly created capability request + or 400 response (HTTPBadRequest) if expected parameters not given + or 412 response (HTTPPreconditionFailed) if capability with given name does not exist and thus cannot be + requested + """ + expected_params = ["parameters", "versions"] + # TODO: What is the versions parameter going to look like? + # We can error check for a well-formatted param once we decide + capability_name = request.matchdict["capability_name"] + params = request.json_body + + if not all([expected in params for expected in expected_params]): + # JSON params do not contain all expected params + params_not_given_msg = ( + f"Expected JSON parameters {expected_params}. Received only {params}." + ) + return HTTPBadRequest(detail=params_not_given_msg) + elif not request.capability_info.lookup_capability(capability_name): + # Capability with given name does not exist; can't create a request for it + does_not_exist_msg = ( + f"Capability {capability_name} does not exist. Cannot create request.", + ) + return HTTPPreconditionFailed(detail=does_not_exist_msg) + else: + new_capability_request = CapabilityRequest( + parameters=params["parameters"], + versions=params["versions"], + capability_name=capability_name, + state="Created", + ) + request.capability_info.save_entity(new_capability_request) + return Response(json_body=new_capability_request.__json__()) + + +@view_config(route_name="edit_capability_request", renderer="json") +def edit_capability_request(request: Request) -> Response: + """ + Pyramid view that accepts a request to edit a capability request + URL: capability/{capability_name}/request/{request_id}/edit + + TODO: Implement once CapabilityVersions are supported + """ + return HTTPBadRequest() diff --git a/services/capability/test/conftest.py b/services/capability/test/conftest.py index 9731368c82b81acb4faf1d4cead1e0fc744bccca..d84c2405674b43c5bb9906639181029c5511d239 100644 --- a/services/capability/test/conftest.py +++ b/services/capability/test/conftest.py @@ -1,10 +1,11 @@ +from typing import Any from unittest.mock import MagicMock import pytest from pyramid.config import Configurator from pyramid.testing import DummyRequest, setUp, tearDown -from workspaces.capability.schema import Capability +from workspaces.capability.schema import Capability, CapabilityRequest @pytest.fixture(scope="module") @@ -46,6 +47,15 @@ def request_null_capability() -> DummyRequest: max_jobs=-1, ), ] + capability_requests = [ + CapabilityRequest( + id=0, + state="Created", + capability_name="null", + parameters="-g", + versions=[], + ) + ] def edit_capability( self, name: str, steps: str = None, max_jobs: int = None, enabled: bool = None @@ -64,14 +74,24 @@ def request_null_capability() -> DummyRequest: return True return False - def lookup_capability(self, capability_name: str): + def lookup_capability(self, capability_name: str) -> Capability: for capability in self.capabilities: if capability_name == capability.name: return capability return None - def save_entity(self, capability: Capability): - self.capabilities.append(capability) + def lookup_capability_request(self, request_id: int) -> CapabilityRequest: + for capability_request in self.capability_requests: + if request_id == capability_request.id: + return capability_request + return None + + def save_entity(self, entity: Any): + if type(entity) is Capability: + self.capabilities.append(entity) + elif type(entity) is CapabilityRequest: + entity.id = len(self.capability_requests) + self.capability_requests.append(entity) request = DummyRequest(capability_info=MockCapabilityInfo()) return request diff --git a/services/capability/test/test_capability_views.py b/services/capability/test/test_capability_views.py index 9fa8feab6c6648d65908f72b650739f51f8b895d..80522dbc4acd1ba4642b9e093a5247733810dd92 100644 --- a/services/capability/test/test_capability_views.py +++ b/services/capability/test/test_capability_views.py @@ -1,3 +1,8 @@ +""" +Tests that test the view functionality of our capability REST API. +The logic can be found in `capability/views/capability.py`. +""" + from pyramid.config import Configurator from pyramid.httpexceptions import ( HTTPBadRequest, diff --git a/shared/workspaces/workspaces/capability/schema.py b/shared/workspaces/workspaces/capability/schema.py index 0905ffb1048c3dce8023d401da71a9dfd05af67f..3eff65e9949a42b53227d9adfb7c78660c1e0e73 100644 --- a/shared/workspaces/workspaces/capability/schema.py +++ b/shared/workspaces/workspaces/capability/schema.py @@ -1,13 +1,17 @@ from __future__ import annotations import json -from typing import Tuple +from typing import Dict, Tuple import sqlalchemy as sa from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship -from workspaces.capability.enums import CapabilityEventType, CapabilityStepType +from workspaces.capability.enums import ( + CapabilityEventType, + CapabilityRequestState, + CapabilityStepType, +) from workspaces.capability.helpers import CapabilitySequence, CapabilityStep from workspaces.capability.helpers_interfaces import CapabilitySequenceIF, ParameterIF from workspaces.capability.schema_interfaces import ( @@ -116,7 +120,7 @@ class Capability(Base, CapabilityIF): self.requests.append(request) return request - def to_dict(self): + def to_dict(self) -> Dict[str, str]: return { "name": self.name, "max_jobs": self.max_jobs, @@ -124,13 +128,13 @@ class Capability(Base, CapabilityIF): "enabled": self.enabled, } - def __str__(self): + def __str__(self) -> str: return ( f"Capability: {self.name}, max concurrent jobs of {self.max_jobs}" f"\nSequence: {self.steps}" ) - def __json__(self): + def __json__(self) -> str: return json.dumps(self.to_dict()) @@ -152,19 +156,20 @@ class CapabilityRequest(Base, CapabilityRequestIF): versions = relationship("CapabilityVersion", back_populates="request") capability = relationship(Capability, back_populates="requests") - def update_status(self, status: str): - # TODO: create field in table - self.status = status + def update_state(self, state: CapabilityRequestState): + self.state = state.name def __str__(self): return f"CapabilityRequest object: {self.__dict__}" - def __json__(self, request) -> dict: - return dict( - id=self.id, - capability_name=self.capability_name, - state=self.state, - parameters=self.parameters, + def __json__(self) -> str: + return json.dumps( + { + "id": self.id, + "capability_name": self.capability_name, + "state": self.state, + "parameters": self.parameters, + } ) diff --git a/shared/workspaces/workspaces/capability/schema_interfaces.py b/shared/workspaces/workspaces/capability/schema_interfaces.py index dd47a5aad44f981984ed6dc2a7bc1564ced6812a..8868fcc59be0b172f9f5c6daa8f425fec3c04838 100644 --- a/shared/workspaces/workspaces/capability/schema_interfaces.py +++ b/shared/workspaces/workspaces/capability/schema_interfaces.py @@ -27,7 +27,7 @@ class CapabilityRequestIF: future_products: str versions: List[CapabilityVersionIF] - def __json__(self, request: CapabilityRequestIF) -> dict: + def __json__(self) -> str: raise NotImplementedError