Skip to content
Snippets Groups Projects
Commit 97a9fb21 authored by Nathan Hertz's avatar Nathan Hertz
Browse files

Added new capability/capability request routes in new `routes.py` file;

added views directory that will hold logic for views
parent 3f8c30dd
No related branches found
No related tags found
1 merge request!73SWS-31: Capability REST API update
Pipeline #520 passed
...@@ -116,7 +116,7 @@ unit test dev notification: ...@@ -116,7 +116,7 @@ unit test dev notification:
- build dev notification - build dev notification
unit test coverage: .unit test coverage:
stage: test-coverage stage: test-coverage
image: python:3.8-slim image: python:3.8-slim
before_script: before_script:
...@@ -260,4 +260,4 @@ clean build notification: ...@@ -260,4 +260,4 @@ clean build notification:
# SWARM_NODE_ENV="test" TAG_TO_DEPLOY="${CI_COMMIT_TAG}" docker stack deploy --compose-file docker-compose.dev.yml workspaces-dev # SWARM_NODE_ENV="test" TAG_TO_DEPLOY="${CI_COMMIT_TAG}" docker stack deploy --compose-file docker-compose.dev.yml workspaces-dev
# rules: # rules:
# - if: $CI_COMMIT_TAG # - if: $CI_COMMIT_TAG
# when: manual # when: manual
\ No newline at end of file
...@@ -4,6 +4,7 @@ pyramid.includes = ...@@ -4,6 +4,7 @@ pyramid.includes =
pyramid_debugtoolbar pyramid_debugtoolbar
pyramid_tm pyramid_tm
pyramid.reload_all = true pyramid.reload_all = true
pyramid.debug_routematch = true
session.cookie_expires = true session.cookie_expires = true
session.auto = true session.auto = true
...@@ -12,7 +13,6 @@ session.auto = true ...@@ -12,7 +13,6 @@ session.auto = true
use = egg:waitress#main use = egg:waitress#main
listen = 0.0.0.0:3457 listen = 0.0.0.0:3457
[loggers] [loggers]
keys = root, capability keys = root, capability
......
from pyramid.config import Configurator
def includeme(config: Configurator):
"""
Function that gets included in server.py:main(); calls all route adding functions
:param config: Pyramid server config object
"""
default_routes(config)
capability_routes(config)
capability_request_routes(config)
def default_routes(config: Configurator):
"""
Uncategorized server routes
:param config: Pyramid server config object
"""
config.add_route("home", "/")
def capability_routes(config: Configurator):
"""
Server routes related to capabilities
:param config: Pyramid server config object
"""
capability_url = "capability/{capability_name}"
# GET
config.add_route(name="view_capability", pattern=f"{capability_url}", request_method="GET")
# POST
config.add_route(
name="create_capability",
pattern="capability/create",
request_method="POST",
)
config.add_route(
name="edit_capability",
pattern=f"{capability_url}/edit",
request_method="POST",
)
config.add_route(
name="deactivate_capability", pattern=f"{capability_url}/deactivate", request_method="POST"
)
def capability_request_routes(config: Configurator):
"""
Server routes related to capability requests
:param config: Pyramid server config object
"""
request_url = "capability/{capability_name}/request/{request_id}"
# 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")
# DELETE
config.add_route("delete_capability_request", f"{request_url}", request_method="DELETE")
...@@ -3,6 +3,7 @@ from pyramid.config import Configurator ...@@ -3,6 +3,7 @@ from pyramid.config import Configurator
from pyramid.events import NewRequest from pyramid.events import NewRequest
from pyramid.renderers import JSONP from pyramid.renderers import JSONP
from pyramid.request import Request from pyramid.request import Request
from pyramid.response import Response
from pyramid.view import view_config, view_defaults from pyramid.view import view_config, view_defaults
from pyramid_beaker import session_factory_from_settings from pyramid_beaker import session_factory_from_settings
...@@ -13,8 +14,22 @@ from workspaces.workflow.services.workflow_service import WorkflowServiceRESTCli ...@@ -13,8 +14,22 @@ from workspaces.workflow.services.workflow_service import WorkflowServiceRESTCli
# Copied from here: https://stackoverflow.com/questions/21107057/pyramid-cors-for-ajax-requests # Copied from here: https://stackoverflow.com/questions/21107057/pyramid-cors-for-ajax-requests
def add_cors_headers_response_callback(event): def add_cors_headers_response_callback(event: NewRequest):
def cors_headers(request, response): """
Event handler that adds CORS HTTP headers to responses from this server; allows external servers
(the front end) to send requests and not have them bounce off
More information about CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
:param event: Server event
"""
def cors_headers(request: Request, response: Response):
"""
Callback function that adds CORS headers to a response
:param response: Server response
"""
response.headers.update( response.headers.update(
{ {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
...@@ -37,9 +52,7 @@ def add_cors_headers_response_callback(event): ...@@ -37,9 +52,7 @@ def add_cors_headers_response_callback(event):
def lookup_request(request): def lookup_request(request):
return request.capability_info.lookup_capability_request( return request.capability_info.lookup_capability_request(request.matchdict["request_id"])
request.matchdict["request_id"]
)
@view_defaults(route_name="capability_request", renderer="json") @view_defaults(route_name="capability_request", renderer="json")
...@@ -54,9 +67,7 @@ class CapabilityRestService: ...@@ -54,9 +67,7 @@ class CapabilityRestService:
request = self.request.capabilities.create_request(req["capability"], req["args"]) request = self.request.capabilities.create_request(req["capability"], req["args"])
return request return request
@view_config( @view_config(request_method="POST", route_name="submit_capability_request", renderer="json")
request_method="POST", route_name="submit_capability_request", renderer="json"
)
def submit(self): def submit(self):
# 1. Submit the request to the service # 1. Submit the request to the service
execution = self.request.capabilities.run_capability(self.request.context) execution = self.request.capabilities.run_capability(self.request.context)
...@@ -93,7 +104,9 @@ def get_tm_session(session_factory, transaction_manager): ...@@ -93,7 +104,9 @@ def get_tm_session(session_factory, transaction_manager):
def main(global_config, **settings): def main(global_config, **settings):
with Configurator(settings=settings) as config: with Configurator(settings=settings) as config:
# Helpers
config.add_subscriber(add_cors_headers_response_callback, NewRequest) config.add_subscriber(add_cors_headers_response_callback, NewRequest)
session_factory = session_factory_from_settings(settings) session_factory = session_factory_from_settings(settings)
config.set_session_factory(session_factory) config.set_session_factory(session_factory)
config.add_renderer("jsonp", JSONP(param_name="callback")) config.add_renderer("jsonp", JSONP(param_name="callback"))
...@@ -139,5 +152,8 @@ def main(global_config, **settings): ...@@ -139,5 +152,8 @@ def main(global_config, **settings):
) )
config.include("pyramid_beaker") config.include("pyramid_beaker")
# Include routes from routes file
config.include(".routes")
# config.scan(".views")
config.scan(".") config.scan(".")
return config.make_wsgi_app() return config.make_wsgi_app()
import json
from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPPreconditionFailed
from pyramid.request import Request
from pyramid.response import Response
from pyramid.view import view_config
from workspaces.capability.schema import Capability
@view_config(route_name="view_capability", renderer="json")
def view_capability(request: Request) -> Response:
"""
Pyramid view that accepts a request to view a capability and responds with the requested capability's
info, if it exists
URL: capability/{capability_name}
:param request: GET request
: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:
return Response(json_body=capability.__json__())
else:
return HTTPNotFound(detail=f"Capability {request.matchdict['capability_name']} not found.")
@view_config(route_name="create_capability", renderer="json")
def create_capability(request: Request) -> Response:
"""
Pyramid view that accepts a request to create a capability
URL: capability/create
: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 already exists
"""
expected_params = ["capability_name", "steps", "max_jobs"]
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 request.capability_info.lookup_capability(params["capability_name"]):
# Capability with given name already exists
already_exists_msg = (
f"Capability {params['capability_name']} already exists.",
f"To instead edit existing capability, use capability/{params['capability_name']}/edit",
)
return HTTPPreconditionFailed(detail=already_exists_msg)
else:
new_capability = Capability(
name=params["capability_name"],
steps=params["steps"],
max_jobs=params["max_jobs"],
)
request.capability_info.save_entity(new_capability)
return Response(json_body=new_capability.__json__())
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
@pytest.fixture(scope="module")
def test_config() -> Configurator:
"""
Returns a dummy Configurator object for testing purposes with set up and teardown
:return: Dummy Configurator
"""
request = DummyRequest()
config = setUp(request=request)
config.add_request_method(
lambda lookup: MagicMock(),
"capability_info",
reify=True,
)
yield config
tearDown()
@pytest.fixture(scope="module")
def request_null_capability() -> DummyRequest:
"""
Returns a dummy request object with a mocked capability_info
:return:
"""
class MockCapabilityInfo(MagicMock):
capabilities = [
Capability(
name="null",
steps="prepare-and-run-workflow null\nawait-parameter qa-status",
max_jobs=2,
)
]
def lookup_capability(self, capability_name: str):
for capability in self.capabilities:
if capability_name == capability.name:
return capability
return None
def save_entity(self, capability: Capability):
self.capabilities.append(capability)
request = DummyRequest(capability_info=MockCapabilityInfo())
return request
from typing import List
import pytest
from pyramid.config import Configurator
from pyramid.interfaces import IRoutesMapper
RouteList = List[str]
@pytest.fixture()
def capability_routes() -> RouteList:
return [
"home",
"view_capability",
"create_capability",
"edit_capability",
"deactivate_capability",
"view_capability_request",
"create_capability_request",
"edit_capability_request",
"submit_capability_request",
"cancel_capability_request",
"delete_capability_request",
]
def test_routes_exist(test_config: Configurator, capability_routes: RouteList):
"""
Tests that the Pyramid config has the proper capability routes set up
:param test_config: Mock pyramid config for testing purposes
"""
test_config.include("capability.routes")
mapper = test_config.registry.queryUtility(IRoutesMapper)
route_names = [route.name for route in mapper.get_routes()]
assert capability_routes == route_names
from pyramid.config import Configurator
from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPPreconditionFailed
from pyramid.testing import DummyRequest
def test_view_capability(test_config: Configurator, request_null_capability: DummyRequest):
"""
Tests the view_capability view to make sure it properly returns the info of a capability
: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 import view_capability
expected_response = '{"name": "null", "max_jobs": 2, "steps": "prepare-and-run-workflow null\\nawait-parameter qa-status"}'
request_null_capability.matchdict["capability_name"] = "null"
response = view_capability(request_null_capability)
assert response.status_code == 200
assert response.json_body == expected_response
def test_view_capability_not_found(
test_config: Configurator, request_null_capability: DummyRequest
):
"""
Tests the view capability view to make sure it properly returns a 404 Not Found exception
when a capability does not exist
: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 import view_capability
request_null_capability.matchdict["capability_name"] = "does_not_exist"
response = view_capability(request_null_capability)
assert response.status_code == 404
assert type(response) is HTTPNotFound
def test_create_capability(test_config: Configurator, request_null_capability: DummyRequest):
"""
Tests the create capability view to make sure it properly supports creation of capabilities
: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 import create_capability
request_null_capability.json_body = {"capability_name": "test", "steps": "test", "max_jobs": 1}
# Assert test capability not in list of capabilities (mocked)
assert not request_null_capability.capability_info.lookup_capability("test")
response = create_capability(request_null_capability)
assert response.status_code == 200
expected_response = '{"name": "test", "max_jobs": 1, "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")
def test_create_capability_error(test_config: Configurator, request_null_capability: DummyRequest):
"""
Tests that the create_capability view correctly responds with exceptions given bad input
: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 import create_capability
request_null_capability.json_body = {"capability_name": "test"}
response_bad_params = create_capability(request_null_capability)
assert response_bad_params.status_code == 400
assert type(response_bad_params) is HTTPBadRequest
request_null_capability.json_body = {"capability_name": "null", "steps": "null", "max_jobs": 69}
response_already_exists = create_capability(request_null_capability)
assert response_already_exists.status_code == 412
assert type(response_already_exists) is HTTPPreconditionFailed
...@@ -17,7 +17,6 @@ from workspaces.capability.schema_interfaces import ( ...@@ -17,7 +17,6 @@ from workspaces.capability.schema_interfaces import (
CapabilityVersionIF, CapabilityVersionIF,
) )
from workspaces.products.schema_interfaces import FutureProductIF from workspaces.products.schema_interfaces import FutureProductIF
from workspaces.workflow.schema import WorkflowRequest
class CapabilityEvent: class CapabilityEvent:
...@@ -127,6 +126,9 @@ class Capability(Base, CapabilityIF): ...@@ -127,6 +126,9 @@ class Capability(Base, CapabilityIF):
f"\nSequence: {self.steps}" f"\nSequence: {self.steps}"
) )
def __json__(self):
return json.dumps(self.to_dict())
class CapabilityRequest(Base, CapabilityRequestIF): class CapabilityRequest(Base, CapabilityRequestIF):
""" """
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment