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 f379c48a8f44f8e8516750ad425f1616fac3e195..8b72b099a57538bfc0fbbee79d39c66083775d99 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 cf7ef8c2d2b65d92c9a2c447c0a6f62ee35397b2..ece8a7caff0c3c21bff6d5b12d00193862da1e84 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 2df97798e1e7e3dcc0d022180644c3d594d70b19..25c02e3f3c0eeb35aed7aa83f531785fea35bb47 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 eb52292a0722b331cd066af4b39f002077050ef0..3ed61379d36bb46ae22a2e87b9015dda18073b0e 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 3d83cb6fa5206ecb878bbdd665f190d59245eaaa..9f4facfb05d3ea3243c9ac50f1652399faec5815 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 76c895aa6bd370e9a310e23b498c14fb01478a35..8565d6d41d9d43e044ee125855c435593a59cae7 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 80522dbc4acd1ba4642b9e093a5247733810dd92..3f61276b7c5e0cf1813ec7c054a227424d8a2d34 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 2e566c8eabfcf5fe11d75d01248d07f4d044eb3c..f052090f99e8e6fdb19dc944e9cb292b9fa4f34d 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 3eff65e9949a42b53227d9adfb7c78660c1e0e73..265e34b4f2bcc9c24c71ce1d95bd57825f0d4c99 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 8868fcc59be0b172f9f5c6daa8f425fec3c04838..e56415cfbe95010a17183f2eb2cbcbe4c93063eb 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