Skip to content

Commit

Permalink
Merge 8c9fddc into 44149e3
Browse files Browse the repository at this point in the history
  • Loading branch information
dougthor42 committed Feb 19, 2019
2 parents 44149e3 + 8c9fddc commit 46c0d13
Show file tree
Hide file tree
Showing 3 changed files with 339 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -13,6 +13,8 @@
+ `GET /api/v1/metric/<metric_name>` has been implemented (#73)
+ `DELETE /api/v1/metric/<metric_name>` has been implemented (#78)
+ `POST /api/v1/metric` has been implemented (#74)
+ `PUT /api/v1/metric/<metric_name>` has been implemented (#75)
+ `PATCH /api/v1/metric/<metric_name>` has been implemented (#83)


## v0.3.0 (2019-01-28)
Expand Down
221 changes: 221 additions & 0 deletions src/trendlines/routes.py
Expand Up @@ -11,7 +11,9 @@

# peewee
from peewee import DoesNotExist
from peewee import IntegrityError
from playhouse.shortcuts import model_to_dict
from playhouse.shortcuts import update_model_from_dict

from trendlines import logger
from . import db
Expand Down Expand Up @@ -247,3 +249,222 @@ def delete_metric(metric):
return resp.as_response(), http_status
else:
return "", 204


@api.route("/api/v1/metric/<metric>", methods=["PUT"])
def put_metric(metric):
"""
Replace a metric with new values.
This function cannot change the ``metric_id`` value.
Keys not given are assumed to be ``None``.
Accepts JSON data with the following format:
.. code-block::json
{
"name": "your.metric_name.here",
"units": {string, optional},
"upper_limit": {float, optional},
"lower_limit": {float, optional},
}
Returns
-------
200 :
Success. Returned JSON data has two keys: ``old_value`` and
``new_value``, each containing a full :class:`orm.Metric` object.
400 :
Malformed JSON data (such as when ``name`` is missing)
404 :
The requested metric is not found.
409 :
The metric already exists.
See Also
--------
:func:`routes.get_metric_as_json`
:func:`routes.post_metric`
:func:`routes.delete_metric`
"""
data = request.get_json()

# First see if our item actually exists
try:
metric = db.Metric.get(db.Metric.name == metric)
old_name = metric.name
old_units = metric.units
old_lower = metric.lower_limit
old_upper = metric.upper_limit
except DoesNotExist:
http_status = 404
detail = "The metric '{}' does not exist".format(metric)
resp = utils.Rfc7807ErrorResponse(
type_="metric-not-found",
title="Metric not found",
status=http_status,
detail=detail,
)
logger.warning("API error: %s" % detail)
return resp.as_response(), http_status

# Parse our json.
# TODO: possible to replace with peewee.dict_to_model?
try:
name = data['name']
except KeyError:
http_status = 400
detail = "Missing required key 'name'."
resp = utils.Rfc7807ErrorResponse(
type_="invalid-request",
title="Missing required JSON key.",
status=http_status,
detail=detail,
)
logger.warning("API error: %s" % detail)
return resp.as_response(), http_status

# All other fields we assume to be None if they're missing.
units = data.get('units', None)
upper_limit = data.get('upper_limit', None)
lower_limit = data.get('lower_limit', None)

# Update the values with the new thingy.
# TODO: use dict_to_model?
metric.name = name
metric.units = units
metric.lower_limit = lower_limit
metric.upper_limit = upper_limit
try:
metric.save()
except IntegrityError:
# Failed the unique constraint on Metric.name
http_status = 409
detail = ("Unable to change metric name '{}': target name '{}'"
" already exists.")
detail = detail.format(old_name, name)
resp = utils.Rfc7807ErrorResponse(
type_="integrity-error",
title="Constraint Failure",
status=http_status,
detail=detail,
)
logger.warning("API error: %s" % detail)
return resp.as_response(), http_status

rv = {
"old_value": {
"name": old_name,
"units": old_units,
"lower_limit": old_lower,
"upper_limit": old_upper,
},
"new_value": {
"name": metric.name,
"units": metric.units,
"lower_limit": metric.lower_limit,
"upper_limit": metric.upper_limit,
},
}

return jsonify(rv), 200


@api.route("/api/v1/metric/<metric>", methods=["PATCH"])
def patch_metric(metric):
"""
Update the values for a given metric.
This cannot change the ``metric_id`` value.
Accepts JSON data with the following format:
.. code-block::json
{
"name": {string, optional},
"units": {string, optional},
"upper_limit": {float, optional},
"lower_limit": {float, optional}
}
Returns
-------
200 :
Success. Returned JSON data has two keys: ``old_value`` and
``new_value``, each containing only the changed items of the
:class:`orm.Metric` object.
404 :
The requested metric is not found.
409 :
The target metric name already exists.
See Also
--------
:func:`routes.get_metric_as_json`
:func:`routes.post_metric`
:func:`routes.delete_metric`
:func:`routes.put_metric`
"""
# XXX: This is essentially the same code as `put`... Gotta refactor ASAP
data = request.get_json()

# First see if our item actually exists
try:
metric = db.Metric.get(db.Metric.name == metric)
old_name = metric.name
old_units = metric.units
old_lower = metric.lower_limit
old_upper = metric.upper_limit
except DoesNotExist:
http_status = 404
detail = "The metric '{}' does not exist".format(metric)
resp = utils.Rfc7807ErrorResponse(
type_="metric-not-found",
title="Metric not found",
status=http_status,
detail=detail,
)
logger.warning("API error: %s" % detail)
return resp.as_response(), http_status

metric = update_model_from_dict(metric, data)

try:
metric.save()
except IntegrityError:
# Failed the unique constraint on Metric.name
http_status = 409
detail = ("Unable to change metric name '{}': target name '{}'"
" already exists.")
detail = detail.format(old_name, metric.name)
resp = utils.Rfc7807ErrorResponse(
type_="integrity-error",
title="Constraint Failure",
status=http_status,
detail=detail,
)
logger.warning("API error: %s" % detail)
return resp.as_response(), http_status

old = {
"name": old_name,
"units": old_units,
"lower_limit": old_lower,
"upper_limit": old_upper,
}
new = {
"name": metric.name,
"units": metric.units,
"lower_limit": metric.lower_limit,
"upper_limit": metric.upper_limit,
}

# This seems... silly.
rv = {'old_value': {}, 'new_value': {}}
for item in data.keys():
rv['old_value'][item] = old[item]
rv['new_value'][item] = new[item]

return jsonify(rv), 200
116 changes: 116 additions & 0 deletions tests/test_routes.py
Expand Up @@ -156,3 +156,119 @@ def test_api_post_metric_missing_key(client, populated_db):
assert rv.is_json
d = rv.get_json()
assert "Missing required" in d['detail']


def test_api_put_metric(client, populated_db):
name = "foo.bar"
data = {
"name": name,
"units": "lines",
"upper_limit": 100,
"lower_limit": 0,
}
rv = client.put("/api/v1/metric/{}".format(name), json=data)
assert rv.status_code == 200
assert rv.is_json
d = rv.get_json()
assert d['old_value']['units'] is None
assert d['new_value']['units'] == "lines"


def test_api_put_metric_not_found(client, populated_db):
name = "missing"
data = {
"name": name,
"units": "lines",
"upper_limit": 100,
"lower_limit": 0,
}
rv = client.put("/api/v1/metric/{}".format(name), json=data)
assert rv.status_code == 404
assert rv.is_json
d = rv.get_json()
assert name in d['detail']
assert "does not exist" in d['detail']


def test_api_put_metric_duplicate_name(client, populated_db):
new_name = "foo.bar"
old_name = "old_data"
data = {"name": new_name}
rv = client.put("/api/v1/metric/{}".format(old_name), json=data)
assert rv.status_code == 409
assert rv.is_json
d = rv.get_json()
assert old_name in d['detail']
assert new_name in d['detail']
assert "Unable to change metric name" in d['detail']


def test_api_put_metric_missing_name(client, populated_db):
data = {"units": "goats"}
rv = client.put("/api/v1/metric/foo", json=data)
assert rv.status_code == 400
assert rv.is_json
d = rv.get_json()
assert "Missing required" in d['detail']


def test_api_put_metric_idempotence(client, populated_db):
# TODO: I don't think this is the right way to test idempotence...
name = "foo.bar"
data = {
"name": name,
"units": "lines",
"upper_limit": 100,
"lower_limit": 0,
}
rv = client.put("/api/v1/metric/{}".format(name), json=data)
assert rv.status_code == 200
assert rv.is_json
d = rv.get_json()
# First verify that things changed.
assert d['old_value']['units'] != d['new_value']

rv = client.put("/api/v1/metric/{}".format(name), json=data)
assert rv.status_code == 200
assert rv.is_json
d = rv.get_json()
# Then verify that they *didn't* change.
assert d['old_value'] == d['new_value']


def test_api_patch_metric(client, populated_db):
data = {"units": "pears"}
rv = client.patch("/api/v1/metric/foo", json=data)
assert rv.status_code == 200
assert rv.is_json
d = rv.get_json()
assert d['old_value']['units'] is None
assert d['new_value']['units'] == "pears"
assert 'name' not in d['old_value'].keys()
assert 'name' not in d['new_value'].keys()
assert 'lower_limit' not in d['old_value'].keys()
assert 'lower_limit' not in d['new_value'].keys()


def test_api_patch_metric_not_found(client, populated_db):
name = "missing"
data = {"units": "pears"}
rv = client.patch("/api/v1/metric/{}".format(name), json=data)
assert rv.status_code == 404
assert rv.is_json
d = rv.get_json()
assert name in d['detail']
assert "does not exist" in d['detail']


def test_api_patch_metric_duplicate_name(client, populated_db):
new_name = "foo.bar"
old_name = "old_data"
data = {"name": new_name}
rv = client.patch("/api/v1/metric/{}".format(old_name), json=data)
assert rv.status_code == 409
assert rv.is_json
d = rv.get_json()
assert old_name in d['detail']
assert new_name in d['detail']
assert "Unable to change metric name" in d['detail']

0 comments on commit 46c0d13

Please sign in to comment.