diff --git a/CHANGELOG.md b/CHANGELOG.md index e91dacd..a5a0426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ + `GET /api/v1/metric/` has been implemented (#73) + `DELETE /api/v1/metric/` has been implemented (#78) + `POST /api/v1/metric` has been implemented (#74) ++ `PUT /api/v1/metric/` has been implemented (#75) ## v0.3.0 (2019-01-28) diff --git a/src/trendlines/routes.py b/src/trendlines/routes.py index 5a0ce8f..c2a322b 100644 --- a/src/trendlines/routes.py +++ b/src/trendlines/routes.py @@ -11,6 +11,7 @@ # peewee from peewee import DoesNotExist +from peewee import IntegrityError from playhouse.shortcuts import model_to_dict from trendlines import logger @@ -247,3 +248,124 @@ def delete_metric(metric): return resp.as_response(), http_status else: return "", 204 + + +@api.route("/api/v1/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 diff --git a/tests/test_routes.py b/tests/test_routes.py index 2e8c72c..3f03c15 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -156,3 +156,81 @@ 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']