Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 48 additions & 5 deletions ceagle/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,70 @@ class UnknownService(Exception):

class Client(base.Client):

def get(self, uri="/", **kwargs):
"""Make GET request and decode JSON data.
def request(self, method="GET", uri="/", **kwargs):
"""Make `method` request and decode JSON data.

:param uri: resource URI
:param kwargs: query parameters
:returns: dict response data
:returns: tuple: response data-dict and response code
"""
url = "%s%s" % (self.endpoint, uri)
try:
response = requests.get(url, **kwargs)
response = requests.request(method, url, **kwargs)
except requests.exceptions.ConnectionError:
mesg = "Service '%(name)s' is not available at '%(endpoint)s'" % (
{"name": self.name, "endpoint": self.endpoint})
return {"error": {"message": mesg}}, 502

# NO_CONTENT means we have nothing to decode
# expected for DELETE methods for example
if response.status_code == 204:
return "", response.status_code

try:
result = response.json()
except ValueError:
return {"error": {"message": "Response can not be decoded"}}, 500
return {"error": {
"message": "Response can not be decoded"}}, 500

return result, response.status_code

def get(self, uri="/", **kwargs):
"""Make GET request and decode JSON data.

:param uri: resource URI
:param kwargs: query parameters
:returns: tuple: response data-dict and response code
"""
return self.request("GET", uri, **kwargs)

def post(self, uri="/", **kwargs):
"""Make POST request and decode JSON data.

:param uri: resource URI
:param kwargs: query parameters
:returns: tuple: response data-dict and response code
"""
return self.request("POST", uri, **kwargs)

def put(self, uri="/", **kwargs):
"""Make PUT request and decode JSON data.

:param uri: resource URI
:param kwargs: query parameters
:returns: tuple: response data-dict and response code
"""
return self.request("PUT", uri, **kwargs)

def delete(self, uri="/", **kwargs):
"""Make DELETE request and decode JSON data.

:param uri: resource URI
:param kwargs: query parameters
:returns: tuple: response data-dict and response code
"""
return self.request("DELETE", uri, **kwargs)


def get_client(service_name):
"""Return client for given service name, if possible.
Expand Down
53 changes: 47 additions & 6 deletions ceagle/api/v1/runbooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import flask

from ceagle.api import client
from ceagle.api_fake_data import fake_runbooks


Expand All @@ -25,35 +26,75 @@
@bp.route("/region/<region>/runbooks",
methods=["GET", "POST"])
@fake_runbooks.handle_runbooks
def handle_runbooks(region=None):
return flask.jsonify("fixme!")
def handle_runbooks(region=''):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method (as well as another methods below) is not covered by unit tests

api_endpoint = "/api/v1/region/{}/runbooks".format(region)

if flask.request.method == "GET":
read_client = client.get_client("runbook-read")
if not region:
api_endpoint = "/api/v1/runbooks"
params = flask.request.args
result, code = read_client.get(api_endpoint, params=params)
else: # POST
write_client = client.get_client("runbook-write")
new_runbook = flask.request.get_json(silent=True) or {}
result, code = write_client.post(api_endpoint, json=new_runbook)
return flask.jsonify(result), code


@bp.route("/region/<region>/runbooks/<book_id>",
methods=["GET", "PUT", "DELETE"])
@fake_runbooks.handle_single_runbook
def handle_single_runbook(region, book_id):
return flask.jsonify("fixme!")
api_endpoint = "/api/v1/region/{}/runbooks/{}".format(
region, book_id)

if flask.request.method == "GET":
read_client = client.get_client("runbook-read")
result, code = read_client.get(api_endpoint)
elif flask.request.method == "PUT":
write_client = client.get_client("runbook-write")
new_runbook = flask.request.get_json(silent=True) or {}
result, code = write_client.put(api_endpoint, json=new_runbook)
elif flask.request.method == "DELETE":
write_client = client.get_client("runbook-write")
result, code = write_client.delete(api_endpoint)
return flask.jsonify(result), code


@bp.route("/region/<region>/runbooks/<book_id>/run",
methods=["POST"])
@fake_runbooks.run_runbook
def run_runbook(region, book_id):
return flask.jsonify("fixme!")
run_client = client.get_client("runbook-run")
run_settings = flask.request.get_json(silent=True) or {}
api_endpoint = "/api/v1/region/{}/runbooks/{}/run".format(region, book_id)
result, code = run_client.post(api_endpoint, json=run_settings)
return flask.jsonify(result), code


@bp.route("/runbook_runs")
@bp.route("/region/<region>/runbook_runs")
@fake_runbooks.runbook_runs
def runbook_runs(region=None):
return flask.jsonify("fixme!")
read_client = client.get_client("runbook-read")
if region:
api_endpoint = "/api/v1/region/{}/runbook_runs".format(region)
else:
api_endpoint = "/api/v1/runbook_runs"
params = flask.request.args
result, code = read_client.get(api_endpoint, params=params)
return flask.jsonify(result), code


@bp.route("/region/<region>/runbook_runs/<run_id>")
@fake_runbooks.single_runbook_run
def single_runbook_run(region, run_id):
return flask.jsonify("fixme!")
read_client = client.get_client("runbook-read")
api_endpoint = "/api/v1/region/{}/runbook_runs/{}".format(region, run_id)
params = flask.request.args
result, code = read_client.get(api_endpoint, params=params)
return flask.jsonify(result), code


def get_blueprints():
Expand Down
3 changes: 3 additions & 0 deletions ceagle/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"health": {"type": "string"},
"optimization": {"type": "string"},
"performance": {"type": "string"},
"runbook-read": {"type": "string"},
"runbook-write": {"type": "string"},
"runbook-run": {"type": "string"},
"security": {"type": "string"},
"infra": {
"type": "object",
Expand Down
69 changes: 51 additions & 18 deletions tests/unit/api/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,41 +30,74 @@ def test___init__(self):
self.assertEqual("foo_ep", ct.endpoint)
self.assertEqual("<Client 'foo'>", repr(ct))

@mock.patch("ceagle.api.client.requests.get")
def test_get(self, mock_requests_get):
mock_requests_get.return_value.status_code = "foo_status"
mock_requests_get.return_value.json.return_value = {"foo": 42}
@mock.patch("ceagle.api.client.requests.request")
def test_get(self, mock_requests):
mock_requests.return_value.status_code = "foo_status"
mock_requests.return_value.json.return_value = {"foo": 42}
ct = client.Client("foo", "http://foo_ep")
result = ct.get()
mock_requests_get.assert_called_once_with("http://foo_ep/")
mock_requests.assert_called_once_with("GET", "http://foo_ep/")
self.assertEqual(({"foo": 42}, "foo_status"), result)

mock_requests_get.reset_mock()

@mock.patch("ceagle.api.client.requests.get")
def test_get_with_path(self, mock_requests_get):
mock_requests_get.return_value.json.return_value = {"foo": 42}
mock_requests_get.return_value.status_code = 200
@mock.patch("ceagle.api.client.requests.request")
def test_get_with_path(self, mock_requests):
mock_requests.return_value.json.return_value = {"foo": 42}
mock_requests.return_value.status_code = 200
ct = client.Client("foo", "http://foo_ep")
result = ct.get("/bar")
mock_requests_get.assert_called_once_with("http://foo_ep/bar")
mock_requests.assert_called_once_with("GET", "http://foo_ep/bar")
self.assertEqual(({"foo": 42}, 200), result)

@mock.patch("ceagle.api.client.requests.get")
def test_get_wrong_response_fmt(self, mock_requests_get):
mock_requests_get.return_value.json.side_effect = ValueError
@mock.patch("ceagle.api.client.requests.request")
def test_get_no_content(self, mock_requests):
mock_requests.return_value.json.return_value = {"foo": 42}
mock_requests.return_value.status_code = 204
ct = client.Client("foo", "http://foo_ep")
result = ct.get("/bar")
mock_requests.assert_called_once_with("GET", "http://foo_ep/bar")
self.assertEqual(("", 204), result)

@mock.patch("ceagle.api.client.requests.request")
def test_get_wrong_response_fmt(self, mock_requests):
mock_requests.return_value.json.side_effect = ValueError
ct = client.Client("foo", "http://foo_ep")
result = ct.get("/bar")
self.assertEqual(
({"error": {"message": "Response can not be decoded"}}, 500),
result)

@mock.patch("ceagle.api.client.requests.get")
def test_get_not_available(self, mock_requests_get):
mock_requests_get.side_effect = (
@mock.patch("ceagle.api.client.requests.request")
def test_get_not_available(self, mock_requests):
mock_requests.side_effect = (
client.requests.exceptions.ConnectionError)
ct = client.Client("foo", "http://foo_ep")
result = ct.get("/bar")
mesg = "Service 'foo' is not available at 'http://foo_ep'"
self.assertEqual(({"error": {"message": mesg}}, 502),
result)

@mock.patch("ceagle.api.client.requests.request")
def test_methods(self, mock_requests):
mock_requests.return_value.json.return_value = {"foo": 42}
mock_requests.return_value.status_code = 200
ct = client.Client("foo", "http://foo_ep")

result = ct.get("/bar", body={}, data={})
mock_requests.assert_called_with("GET", "http://foo_ep/bar",
body={}, data={})
self.assertEqual(({"foo": 42}, 200), result)

result = ct.put("/bar", body={}, data={})
mock_requests.assert_called_with("PUT", "http://foo_ep/bar",
body={}, data={})
self.assertEqual(({"foo": 42}, 200), result)

result = ct.post("/bar", body={}, data={})
mock_requests.assert_called_with("POST", "http://foo_ep/bar",
body={}, data={})
self.assertEqual(({"foo": 42}, 200), result)

result = ct.delete("/bar", body={}, data={})
mock_requests.assert_called_with("DELETE", "http://foo_ep/bar",
body={}, data={})
self.assertEqual(({"foo": 42}, 200), result)