From 30347c2a2ef4c3d7635b64fb4ac6bdaa4abe14e7 Mon Sep 17 00:00:00 2001 From: Nathan Hertz <nhertz@nrao.edu> Date: Mon, 22 Feb 2021 10:57:58 -0500 Subject: [PATCH] Added new Pyramid view: submit_capability_request --- .../services/capability-launcher.service.ts | 11 ++- .../app/workspaces/workspaces.component.ts | 23 ++--- services/capability/capability/server.py | 88 ++++++++----------- .../capability/views/capability_request.py | 33 ++++++- services/capability/test/conftest.py | 32 ++++++- .../test/test_capability_request_views.py | 43 ++++++++- .../capability/test/test_capability_views.py | 4 +- shared/workspaces/setup.py | 1 + .../workspaces/capability/schema.py | 35 ++++---- .../capability/schema_interfaces.py | 6 +- 10 files changed, 176 insertions(+), 100 deletions(-) diff --git a/apps/web/src/app/workspaces/services/capability-launcher.service.ts b/apps/web/src/app/workspaces/services/capability-launcher.service.ts index f379c48a8..8b72b099a 100644 --- a/apps/web/src/app/workspaces/services/capability-launcher.service.ts +++ b/apps/web/src/app/workspaces/services/capability-launcher.service.ts @@ -21,7 +21,7 @@ export class CapabilityLauncherService { createRequest( capabilityName: string, parameters: string - ): Observable<string> { + ): Observable<CapabilityRequest> { const url = environment.workspacesUrl + this.endpoint + @@ -30,15 +30,18 @@ export class CapabilityLauncherService { const requestParams = JSON.stringify({ parameters: parameters, }); - return this.httpClient.post<string>(url, requestParams); + return this.httpClient.post<CapabilityRequest>(url, requestParams); } /** * Submit capability request * @param: requestId ID of capability request to submit */ - submit(requestId: string): Observable<CapabilityExecution> { - const url = `${environment.workspacesUrl}${this.endpoint}request/${requestId}/submit`; + submit( + capabilityName: string, + requestId: string + ): Observable<CapabilityExecution> { + const url = `${environment.workspacesUrl}${this.endpoint}${capabilityName}/request/${requestId}/submit`; return this.httpClient.post<CapabilityExecution>(url, null); } } diff --git a/apps/web/src/app/workspaces/workspaces.component.ts b/apps/web/src/app/workspaces/workspaces.component.ts index cf7ef8c2d..ece8a7caf 100644 --- a/apps/web/src/app/workspaces/workspaces.component.ts +++ b/apps/web/src/app/workspaces/workspaces.component.ts @@ -42,19 +42,20 @@ export class WorkspacesComponent implements OnInit { // Create capability request this.capabilityLauncher.createRequest(capabilityName, parameters).subscribe( (requestResponse) => { - const capabilityRequest = JSON.parse(requestResponse); - this.capabilityRequests.push(capabilityRequest); - if (capabilityRequest.id) { + this.capabilityRequests.push(requestResponse); + if (requestResponse.id) { // Capability request created; ID found // Submit capability request - this.capabilityLauncher.submit(capabilityRequest.id).subscribe( - (submitResponse) => { - this.capabilityExecutions.push(submitResponse); - }, - (error) => { - console.log(error); - } - ); + this.capabilityLauncher + .submit(capabilityName, requestResponse.id) + .subscribe( + (submitResponse) => { + this.capabilityExecutions.push(submitResponse); + }, + (error) => { + console.log(error); + } + ); } }, (error) => { diff --git a/services/capability/capability/server.py b/services/capability/capability/server.py index 2df97798e..25c02e3f3 100644 --- a/services/capability/capability/server.py +++ b/services/capability/capability/server.py @@ -4,8 +4,8 @@ from pyramid.events import NewRequest from pyramid.renderers import JSONP from pyramid.request import Request from pyramid.response import Response -from pyramid.view import view_config, view_defaults -from pyramid_beaker import session_factory_from_settings +from pyramid_beaker import BeakerSessionFactoryConfig, session_factory_from_settings +from transaction import TransactionManager from workspaces.capability.services.capability_info import CapabilityInfo from workspaces.capability.services.capability_service import CapabilityService @@ -51,29 +51,39 @@ def add_cors_headers_response_callback(event: NewRequest): # --------------------------------------------------------- -def lookup_request(request): - return request.capability_info.lookup_capability_request(request.matchdict["request_id"]) - - -@view_defaults(route_name="capability_request", renderer="json") -class CapabilityRestService: - def __init__(self, request: Request): - self.request = request +def add_services(config: Configurator, session_factory: BeakerSessionFactoryConfig) -> Configurator: + """ + Add capability info, capability service, and workflow service to Pyramid request configuration - @view_config(request_method="POST", renderer="json") - def create(self): - # create a capability request for this ... request - req = self.request.json_body - request = self.request.capabilities.create_request(req["capability"], req["args"]) - return request + :param config: Pyramid Configurator object + :param session_factory: Pyramid Beaker session factory + :return: Updated Configurator + """ + # make capability_info available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda request: CapabilityInfo(get_tm_session(session_factory, request.tm)), + "capability_info", + reify=True, + ) + # make workflow_info available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda request: WorkflowServiceRESTClient(), + "workflow_service", + reify=True, + ) + # make capability_service available for use in Pyramid + config.add_request_method( + lambda request: CapabilityService(request.capability_info, request.workflow_service), + "capability_service", + reify=True, + ) + return config - @view_config(request_method="POST", route_name="submit_capability_request", renderer="json") - def submit(self): - # 1. Submit the request to the service - execution = self.request.capabilities.run_capability(self.request.context) - # 2. Return something we can listen for - return execution +def lookup_request(request): + return request.capability_info.lookup_capability_request(request.matchdict["request_id"]) # --------------------------------------------------------- @@ -83,7 +93,9 @@ class CapabilityRestService: # --------------------------------------------------------- -def get_tm_session(session_factory, transaction_manager): +def get_tm_session( + session_factory: BeakerSessionFactoryConfig, transaction_manager: TransactionManager +): """ Enable Zope's transaction manager on our session :param session_factory: @@ -122,34 +134,8 @@ def main(global_config, **settings): session_factory = get_session_factory(get_engine()) config.registry["dbsession_factory"] = session_factory - # make capability_info available for use in Pyramid - config.add_request_method( - # r.tm is the transaction manager used by pyramid_tm - lambda request: CapabilityInfo(get_tm_session(session_factory, request.tm)), - "capability_info", - reify=True, - ) - # make workflow_info available for use in Pyramid - config.add_request_method( - # r.tm is the transaction manager used by pyramid_tm - lambda request: WorkflowServiceRESTClient(), - "workflow_service", - reify=True, - ) - # make capability_service available for use in Pyramid - config.add_request_method( - lambda r: CapabilityService(r.capability_info, r.workflow_service), - "capabilities", - reify=True, - ) - - # add some routes - config.add_route("capability_request", "/capability/request") - config.add_route( - "submit_capability_request", - "/capability/request/{request_id}/submit", - factory=lookup_request, - ) + # Add services to config + config = add_services(config, session_factory) config.include("pyramid_beaker") # Include routes from routes file diff --git a/services/capability/capability/views/capability_request.py b/services/capability/capability/views/capability_request.py index eb52292a0..3ed61379d 100644 --- a/services/capability/capability/views/capability_request.py +++ b/services/capability/capability/views/capability_request.py @@ -41,7 +41,7 @@ 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"] + :param request: POST request, expecting JSON parameter "parameters" :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 @@ -65,7 +65,7 @@ def create_capability_request(request: Request) -> Response: return HTTPPreconditionFailed(detail=does_not_exist_msg) else: # TODO: Implement future products - new_capability_request = request.capabilities.create_request( + new_capability_request = request.capability_service.create_request( capability_name, parameters=params["parameters"], products=None ) return Response(json_body=new_capability_request.__json__()) @@ -77,6 +77,33 @@ 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 + :param request: POST request + :return Bad Request response, since it's not yet implemented + TODO: Implement once CapabilityVersions are supported """ - return HTTPBadRequest() + return HTTPBadRequest(detail="Editing capability requests is not yet implemented.") + + +@view_config(route_name="submit_capability_request", renderer="json") +def submit_capability_request(request: Request) -> Response: + """ + Pyramid view that accepts a request to submit a capability request + URL: capability/{capability_name}/request/{request_id}/submit + + :param request: POST request + :return: Response with + or 412 response (HTTPPreconditionFailed) if capability request with given ID does not exist and thus cannot be + submitted + """ + capability_name = request.matchdict["capability_name"] + request_id = request.matchdict["request_id"] + capability_request = request.capability_info.lookup_capability_request(request_id) + + if not capability_request: + # Capability request not found + does_not_exist_msg = f"Capability request for {capability_name} with ID {request_id} does not exist. Cannot submit request." + return HTTPPreconditionFailed(detail=does_not_exist_msg) + else: + execution = request.capability_service.run_capability(capability_request) + return Response(json_body=execution.__json__()) diff --git a/services/capability/test/conftest.py b/services/capability/test/conftest.py index 3d83cb6fa..9f4facfb0 100644 --- a/services/capability/test/conftest.py +++ b/services/capability/test/conftest.py @@ -5,10 +5,11 @@ import pytest from pyramid.config import Configurator from pyramid.testing import DummyRequest, setUp, tearDown -from workspaces.capability.enums import CapabilityRequestState +from workspaces.capability.enums import CapabilityRequestState, ExecutionState from workspaces.capability.helpers import Parameter from workspaces.capability.schema import ( Capability, + CapabilityExecution, CapabilityRequest, CapabilityVersion, ) @@ -40,11 +41,13 @@ class MockCapabilityInfo(MagicMock): name="null", steps="test", max_jobs=2, + enabled=True, ), Capability( name="error", steps="error", max_jobs=-1, + enabled=True, ), ] capability_requests = [ @@ -56,6 +59,7 @@ class MockCapabilityInfo(MagicMock): versions=[], ) ] + capability_executions = [] def edit_capability( self, name: str, steps: str = None, max_jobs: int = None, enabled: bool = None @@ -92,6 +96,9 @@ class MockCapabilityInfo(MagicMock): elif type(entity) is CapabilityRequest: entity.id = len(self.capability_requests) self.capability_requests.append(entity) + elif type(entity) is CapabilityExecution: + entity.id = len(self.capability_executions) + self.capability_executions.append(entity) class MockCapabilityService(MagicMock): @@ -125,6 +132,27 @@ class MockCapabilityService(MagicMock): self.capability_info.save_entity(request) return request + def run_capability(self, capability_request: CapabilityRequest) -> CapabilityExecution: + """ + Mock run_capability method + + :param capability_request: Request to make an execution for + :return: Mocked execution + """ + execution = CapabilityExecution( + state=ExecutionState.Ready.name, + version=CapabilityVersion( + capability_request_id=capability_request.id, + version_number=1, + parameters=capability_request.parameters, + request=capability_request, + ), + current_step=0, + steps="test", + ) + self.capability_info.save_entity(execution) + return execution + @pytest.fixture(scope="module") def request_null_capability() -> DummyRequest: @@ -137,6 +165,6 @@ def request_null_capability() -> DummyRequest: mock_capability_info = MockCapabilityInfo() request = DummyRequest( capability_info=mock_capability_info, - capabilities=MockCapabilityService(mock_capability_info), + capability_service=MockCapabilityService(mock_capability_info), ) return request diff --git a/services/capability/test/test_capability_request_views.py b/services/capability/test/test_capability_request_views.py index 76c895aa6..8565d6d41 100644 --- a/services/capability/test/test_capability_request_views.py +++ b/services/capability/test/test_capability_request_views.py @@ -23,9 +23,7 @@ def test_view_capability_request(test_config: Configurator, request_null_capabil """ from capability.views.capability_request import view_capability_request - expected_response = ( - '{"id": 0, "capability_name": "null", "state": "Created", "parameters": "-g"}' - ) + expected_response = {"capability_name": "null", "id": 0, "parameters": "-g", "state": "Created"} request_null_capability.matchdict["capability_name"] = "null" request_null_capability.matchdict["request_id"] = 0 response = view_capability_request(request_null_capability) @@ -72,7 +70,7 @@ def test_create_capability_request( response = create_capability_request(request_null_capability) assert response.status_code == 200 - expected_response = '{"id": 1, "capability_name": "null", "state": "Ready", "parameters": "-g"}' + expected_response = {"capability_name": "null", "id": 1, "parameters": "-g", "state": "Ready"} assert response.json_body == expected_response # Assert test capability has been added to list of capabilities (mocked) assert request_null_capability.capability_info.lookup_capability_request(1) @@ -100,3 +98,40 @@ def test_create_capability_request_error( response_does_not_exist = create_capability_request(request_null_capability) assert response_does_not_exist.status_code == 412 assert type(response_does_not_exist) is HTTPPreconditionFailed + + +def test_submit_capability_request( + test_config: Configurator, request_null_capability: DummyRequest +): + """ + Tests that the submit_capability_request view correctly executes its logic + + :param test_config: Dummy Pyramid Configurator object set up for testing + :param request_null_capability: Dummy Pyramid request object set up with mocked DB access + supporting the null capability + """ + from capability.views.capability_request import submit_capability_request + + request_null_capability.matchdict["capability_name"] = "null" + request_null_capability.matchdict["request_id"] = 0 + response = submit_capability_request(request_null_capability) + assert response.status_code == 200 + + +def test_submit_capability_request_error( + test_config: Configurator, request_null_capability: DummyRequest +): + """ + Tests that the submit_capability_request view correctly executes its logic + + :param test_config: Dummy Pyramid Configurator object set up for testing + :param request_null_capability: Dummy Pyramid request object set up with mocked DB access + supporting the null capability + """ + from capability.views.capability_request import submit_capability_request + + request_null_capability.matchdict["capability_name"] = "null" + request_null_capability.matchdict["request_id"] = -1 + response = submit_capability_request(request_null_capability) + assert response.status_code == 412 + assert type(response) is HTTPPreconditionFailed diff --git a/services/capability/test/test_capability_views.py b/services/capability/test/test_capability_views.py index 80522dbc4..3f61276b7 100644 --- a/services/capability/test/test_capability_views.py +++ b/services/capability/test/test_capability_views.py @@ -23,7 +23,7 @@ def test_view_capability(test_config: Configurator, request_null_capability: Dum """ from capability.views.capability import view_capability - expected_response = '{"name": "null", "max_jobs": 2, "steps": "test", "enabled": null}' + expected_response = {"enabled": True, "max_jobs": 2, "name": "null", "steps": "test"} request_null_capability.matchdict["capability_name"] = "null" response = view_capability(request_null_capability) assert response.status_code == 200 @@ -70,7 +70,7 @@ def test_create_capability(test_config: Configurator, request_null_capability: D response = create_capability(request_null_capability) assert response.status_code == 200 - expected_response = '{"name": "test_create", "max_jobs": 1, "steps": "test", "enabled": true}' + expected_response = {"enabled": True, "max_jobs": 1, "name": "test_create", "steps": "test"} assert response.json_body == expected_response # Assert test capability has been added to list of capabilities (mocked) assert request_null_capability.capability_info.lookup_capability("test_create") diff --git a/shared/workspaces/setup.py b/shared/workspaces/setup.py index 2e566c8ea..f052090f9 100644 --- a/shared/workspaces/setup.py +++ b/shared/workspaces/setup.py @@ -16,6 +16,7 @@ requires = [ "cx-Oracle", "chevron", "requests", + "transaction", ] setup( diff --git a/shared/workspaces/workspaces/capability/schema.py b/shared/workspaces/workspaces/capability/schema.py index 3eff65e99..265e34b4f 100644 --- a/shared/workspaces/workspaces/capability/schema.py +++ b/shared/workspaces/workspaces/capability/schema.py @@ -120,22 +120,19 @@ class Capability(Base, CapabilityIF): self.requests.append(request) return request - def to_dict(self) -> Dict[str, str]: - return { - "name": self.name, - "max_jobs": self.max_jobs, - "steps": str(self.steps), - "enabled": self.enabled, - } - def __str__(self) -> str: return ( f"Capability: {self.name}, max concurrent jobs of {self.max_jobs}" f"\nSequence: {self.steps}" ) - def __json__(self) -> str: - return json.dumps(self.to_dict()) + def __json__(self) -> Dict[str, str]: + return { + "name": self.name, + "max_jobs": self.max_jobs, + "steps": str(self.steps), + "enabled": self.enabled, + } class CapabilityRequest(Base, CapabilityRequestIF): @@ -162,15 +159,13 @@ class CapabilityRequest(Base, CapabilityRequestIF): def __str__(self): return f"CapabilityRequest object: {self.__dict__}" - def __json__(self) -> str: - return json.dumps( - { - "id": self.id, - "capability_name": self.capability_name, - "state": self.state, - "parameters": self.parameters, - } - ) + def __json__(self) -> Dict[str, str]: + return { + "id": self.id, + "capability_name": self.capability_name, + "state": self.state, + "parameters": self.parameters, + } class CapabilityVersion(Base, CapabilityVersionIF): @@ -222,7 +217,7 @@ class CapabilityExecution(Base, CapabilityExecutionIF): ), ) - def __json__(self, request: CapabilityRequest) -> dict: + def __json__(self) -> dict: return dict( id=self.id, state=self.state, diff --git a/shared/workspaces/workspaces/capability/schema_interfaces.py b/shared/workspaces/workspaces/capability/schema_interfaces.py index 8868fcc59..e56415cfb 100644 --- a/shared/workspaces/workspaces/capability/schema_interfaces.py +++ b/shared/workspaces/workspaces/capability/schema_interfaces.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List +from typing import Dict, List from workspaces.capability.helpers_interfaces import CapabilityStepIF, ParameterIF from workspaces.products.schema_interfaces import FutureProductIF @@ -27,7 +27,7 @@ class CapabilityRequestIF: future_products: str versions: List[CapabilityVersionIF] - def __json__(self) -> str: + def __json__(self) -> Dict[str, str]: raise NotImplementedError @@ -48,5 +48,5 @@ class CapabilityExecutionIF: capability: CapabilityIF capability_request: CapabilityRequestIF - def __json__(self, request: CapabilityRequestIF) -> dict: + def __json__(self) -> Dict[str, str]: raise NotImplementedError -- GitLab