<commit_before># -*- encoding: utf-8 -*-
#
# Copyright © 2014 eNovance
#
# Authors: <NAME> <<EMAIL>>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import functools
import json
import uuid

import iso8601
from oslo.utils import strutils
from oslo.utils import timeutils
import pecan
from pecan import rest
from pytimeparse import timeparse
import six
from six.moves.urllib import parse as urllib_parse
from stevedore import extension
import voluptuous
import webob.exc
import werkzeug.http

from gnocchi import aggregates
from gnocchi import archive_policy
from gnocchi import indexer
from gnocchi.openstack.common import policy
from gnocchi import storage

LOGICAL_AND = '∧'


_ENFORCER = None


def enforce(rule, target):
    """Return the user and project the request should be limited to.

    :param rule: The rule name
    :param target: The target to enforce on.

    """
    global _ENFORCER
    if not _ENFORCER:
        _ENFORCER = policy.Enforcer()

    headers = pecan.request.headers

    # NOTE(jd) If user_id or project_id are UUID, try to convert them in the
    # proper dashed format. It's indeed possible that a middleware passes
    # theses UUID without the dash representation. It's valid, we can parse,
    # but the policy module won't see the equality in the string
    # representations.

    user_id = headers.get("X-User-Id")
    try:
        user_id = six.text_type(uuid.UUID(user_id))
    except Exception:
        pass

    project_id = headers.get("X-Project-Id")
    try:
        project_id = six.text_type(uuid.UUID(project_id))
    except Exception:
        pass

    creds = {
        'roles': headers.get("X-Roles", "").split(","),
        'user_id': user_id,
        'project_id': project_id
    }

    if not _ENFORCER.enforce(rule, target, creds):
        pecan.abort(403)


def set_resp_location_hdr(location):
    # NOTE(sileht): according the pep-3333 the headers must be
    # str in py2 and py3 even this is not the same thing in both
    # version
    # see: http://legacy.python.org/dev/peps/pep-3333/#unicode-issues
    if six.PY2 and isinstance(location, six.text_type):
        location = location.encode('utf-8')
    location = urllib_parse.quote(location)
    pecan.response.headers['Location'] = location


def get_user_and_project():
    return (pecan.request.headers.get('X-User-Id'),
            pecan.request.headers.get('X-Project-Id'))


def deserialize(schema):
    mime_type, options = werkzeug.http.parse_options_header(
        pecan.request.headers.get('Content-Type'))
    if mime_type != "application/json":
        pecan.abort(415)
    try:
        params = json.loads(pecan.request.body.decode(
            options.get('charset', 'ascii')))
    except Exception as e:
        pecan.abort(400, "Unable to decode body: " + str(e))
    try:
        return schema(params)
    except voluptuous.Error as e:
        pecan.abort(400, "Invalid input: %s" % e)


def vexpose(schema, *vargs, **vkwargs):
    def expose(f):
        f = pecan.expose(*vargs, **vkwargs)(f)

        @functools.wraps(f)
        def callfunction(*args, **kwargs):
            return f(*args, body=deserialize(schema), **kwargs)
        return callfunction
    return expose


def Timestamp(v):
    if v is None:
        return v
    try:
        v = float(v)
    except (ValueError, TypeError):
        return timeutils.normalize_time(iso8601.parse_date(v))
    return datetime.datetime.utcfromtimestamp(v)


def convert_metric_list(metrics, created_by_user_id, created_by_project_id):
    # Replace an archive policy as value for an metric by a brand
    # a new metric
    new_metrics = {}
    for k, v in six.iteritems(metrics):
        if isinstance(v, uuid.UUID):
            new_metrics[k] = v
        else:
            new_metrics[k] = str(MetricsController.create_metric(
                created_by_user_id, created_by_project_id,
                v['archive_policy_name']))
    return new_metrics


def PositiveOrNullInt(value):
    value = int(value)
    if value < 0:
        raise ValueError("Value must be positive")
    return value


def PositiveNotNullInt(value):
    value = int(value)
    if value <= 0:
        raise ValueError("Value must be positive and not null")
    return value


def Timespan(value):
    if value is None:
        raise ValueError("Invalid timespan")
    try:
        seconds = int(value)
    except Exception:
        try:
            seconds = timeparse.timeparse(six.text_type(value))
        except Exception:
            raise ValueError("Unable to parse timespan")
    if seconds is None:
        raise ValueError("Unable to parse timespan")
    if seconds <= 0:
        raise ValueError("Timespan must be positive")
    return seconds


def get_details(params):
    type, options = werkzeug.http.parse_options_header(
        pecan.request.headers.get('Accept'))
    try:
        details = strutils.bool_from_string(
            options.get('details', params.pop('details', 'false')),
            strict=True)
    except ValueError as e:
        method = 'Accept' if 'details' in options else 'query'
        pecan.abort(
            400,
            "Unable to parse details value in %s: %s" % (method, str(e)))
    return details


class ArchivePoliciesController(rest.RestController):
    ArchivePolicy = voluptuous.Schema({
        voluptuous.Required("name"): six.text_type,
        voluptuous.Required("back_window", default=0): PositiveOrNullInt,
        voluptuous.Required("definition"):
        voluptuous.All([{
            "granularity": Timespan,
            "points": PositiveNotNullInt,
            "timespan": Timespan,
            }], voluptuous.Length(min=1)),
        })

    @staticmethod
    @vexpose(ArchivePolicy, 'json')
    def post(body):
        # Validate the data
        try:
            ap = archive_policy.ArchivePolicy.from_dict(body)
        except ValueError as e:
            pecan.abort(400, e)
        enforce("create archive policy", ap.to_dict())
        try:
            ap = pecan.request.indexer.create_archive_policy(ap)
        except indexer.ArchivePolicyAlreadyExists as e:
            pecan.abort(409, e)

        location = "/v1/archive_policy/" + ap['name']
        set_resp_location_hdr(location)
        pecan.response.status = 201
        return archive_policy.ArchivePolicy.from_dict(
            ap).to_human_readable_dict()

    @pecan.expose('json')
    def get_one(self, id):
        ap = pecan.request.indexer.get_archive_policy(id)
        if ap:
            enforce("get archive policy", ap)
            return archive_policy.ArchivePolicy.from_dict(
                ap).to_human_readable_dict()
        pecan.abort(404)

    @pecan.expose('json')
    def get_all(self):
        enforce("list archive policy", {})
        return [
            archive_policy.ArchivePolicy.from_dict(
                ap).to_human_readable_dict()
            for ap in pecan.request.indexer.list_archive_policies()
        ]

    @pecan.expose()
    def delete(self, name):
        try:
            pecan.request.indexer.delete_archive_policy(name)
        except indexer.NoSuchArchivePolicy as e:
            pecan.abort(404, e)
        except indexer.ArchivePolicyInUse as e:
            pecan.abort(400, e)


class AggregatedMetricController(rest.RestController):
    _custom_actions = {
        'measures': ['GET']
    }

    def __init__(self, metric_ids):
        self.metric_ids = metric_ids

    @pecan.expose('json')
    def get_measures(self, start=None, stop=None, aggregation='mean',
                     needed_overlap=100.0):
        if (aggregation
           not in archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS):
            pecan.abort(
                400,
                'Invalid aggregation value %s, must be one of %s'
                % (aggregation,
                   archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS))

        # Check RBAC policy
        metrics = pecan.request.indexer.get_metrics(self.metric_ids)
        missing_metric_ids = (set(m['id'] for m in metrics)
                              - set(self.metric_ids))
        if missing_metric_ids:
            # Return the first missing one in the error
            pecan.abort(404, storage.MetricDoesNotExist(
                missing_metric_ids[0]))

        for metric in metrics:
            enforce("get metric", metric)

        try:
            measures = pecan.request.storage.get_cross_metric_measures(
                self.metric_ids, start, stop, aggregation, needed_overlap)
            # Replace timestamp keys by their string versions
            return [(timeutils.strtime(timestamp), offset, v)
                    for timestamp, offset, v in measures]
        except storage.MetricUnaggregatable:
            pecan.abort(400, "One of the metric to aggregated doesn't have "
                        "matching granularity")
        except storage.MetricDoesNotExist as e:
            pecan.abort(404, str(e))


class MetricController(rest.RestController):
    _custom_actions = {
        'measures': ['POST', 'GET']
    }

    def __init__(self, metric_id):
        self.metric_id = metric_id
        mgr = extension.ExtensionManager(namespace='gnocchi.aggregates',
                                         invoke_on_load=True)
        self.custom_agg = dict((x.name, x.obj) for x in mgr)

    Measures = voluptuous.Schema([{
        voluptuous.Required("timestamp"):
        Timestamp,
        voluptuous.Required("value"): voluptuous.Any(float, int),
    }])

    def enforce_metric(self, rule):
        metrics = pecan.request.indexer.get_metrics((self.metric_id,))
        if not metrics:
            pecan.abort(404, storage.MetricDoesNotExist(self.metric_id))
        enforce(rule, metrics[0])

    @pecan.expose('json')
    def get_all(self, **kwargs):
        details = get_details(kwargs)
        metrics = pecan.request.indexer.get_metrics((self.metric_id,),
                                                    details=details)
        if not metrics:
            pecan.abort(404, storage.MetricDoesNotExist(self.metric_id))

        metric = metrics[0]
        enforce("get metric", metric)

        if details:
            metric['archive_policy'] = (
                archive_policy.ArchivePolicy.from_dict(
                    metric['archive_policy']
                ).to_human_readable_dict())
        return metric

    @vexpose(Measures)
    def post_measures(self, body):
        self.enforce_metric("post measures")
        try:
            pecan.request.storage.add_measures(
                self.metric_id,
                (storage.Measure(
                    m['timestamp'],
                    m['value']) for m in body))
        except storage.MetricDoesNotExist as e:
            pecan.abort(404, str(e))
        except storage.NoDeloreanAvailable as e:
            pecan.abort(400,
                        "The measure for %s is too old considering the "
                        "archive policy used by this metric. "
                        "It can only go back to %s."
                        % (e.bad_timestamp, e.first_timestamp))

    @pecan.expose('json')
    def get_measures(self, start=None, stop=None, aggregation='mean', **param):
        self.enforce_metric("get measures")
        if not (aggregation
                in archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS
                or aggregation in self.custom_agg):
            msg = '''Invalid aggregation value %(agg)s, must be one of %(std)s
                     or %(custom)s'''
            pecan.abort(400, msg % dict(
                agg=aggregation,
                std=archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS,
                custom=str(self.custom_agg.keys())))

        if start is not None:
            try:
                start = Timestamp(start)
            except Exception:
                pecan.abort(400, "Invalid value for start")

        if stop is not None:
            try:
                stop = Timestamp(stop)
            except Exception:
                pecan.abort(400, "Invalid value for stop")

        try:
            if aggregation in self.custom_agg:
                measures = self.custom_agg[aggregation].compute(
                    pecan.request.storage, self.metric_id, start, stop,
                    **param)
            else:
                measures = pecan.request.storage.get_measures(self.metric_id,
                                                              start, stop,
                                                              aggregation)
            # Replace timestamp keys by their string versions
            return [(timeutils.strtime(timestamp), offset, v)
                    for timestamp, offset, v in measures]
        except storage.MetricDoesNotExist as e:
            pecan.abort(404, str(e))
        except aggregates.CustomAggFailure as e:
            pecan.abort(400, str(e))

    @pecan.expose()
    def delete(self):
        self.enforce_metric("delete metric")
        try:
            pecan.request.storage.delete_metric(self.metric_id)
        except storage.MetricDoesNotExist as e:
            pecan.abort(404, str(e))
        pecan.request.indexer.delete_metric(self.metric_id)


def UUID(value):
    try:
        return uuid.UUID(value)
    except Exception as e:
        raise ValueError(e)


MetricSchemaDefinition = {
    "user_id": UUID,
    "project_id": UUID,
    voluptuous.Required('archive_policy_name'): six.text_type,
}


class MetricsController(rest.RestController):
    @staticmethod
    @pecan.expose()
    def _lookup(id, *remainder):
        # That's triggered when accessing /v1/metric/
        if id is "":
            pecan.abort(404)
        return MetricController(id), remainder

    Metric = voluptuous.Schema(MetricSchemaDefinition)

    @staticmethod
    def create_metric(created_by_user_id, created_by_project_id,
                      archive_policy_name,
                      user_id=None, project_id=None):
        enforce("create metric", {
            "created_by_user_id": created_by_user_id,
            "created_by_project_id": created_by_project_id,
            "user_id": user_id,
            "project_id": project_id,
            "archive_policy_name": archive_policy_name,
        })
        id = uuid.uuid4()
        policy = pecan.request.indexer.get_archive_policy(archive_policy_name)
        if policy is None:
            pecan.abort(400, "Unknown archive policy %s" % archive_policy_name)
        ap = archive_policy.ArchivePolicy.from_dict(policy)
        pecan.request.indexer.create_metric(
            id,
            created_by_user_id, created_by_project_id,
            archive_policy_name=policy['name'])
        pecan.request.storage.create_metric(str(id), ap)
        return id

    @vexpose(Metric, 'json')
    def post(self, body):
        user, project = get_user_and_project()
        id = self.create_metric(user, project, **body)
        set_resp_location_hdr("/v1/metric/" + str(id))
        pecan.response.status = 201
        return {"id": str(id),
                "archive_policy_name": str(body['archive_policy_name'])}

    @pecan.expose('json')
    def get_all(self, **kwargs):
        try:
            enforce("list all metric", {})
        except webob.exc.HTTPForbidden:
            enforce("list metric", {})
            user_id, project_id = get_user_and_project()
            provided_user_id = kwargs.get('user_id')
            provided_project_id = kwargs.get('project_id')
            if ((provided_user_id and user_id != provided_user_id)
               or (provided_project_id and project_id != provided_project_id)):
                pecan.abort(
                    403, "Insufficient privileges to filter by user/project")
        else:
            user_id = kwargs.get('user_id')
            project_id = kwargs.get('project_id')
        return pecan.request.indexer.list_metrics(
            user_id, project_id)


def UUID(value):
    try:
        return uuid.UUID(value)
    except Exception as e:
        raise ValueError(e)


Metrics = voluptuous.Schema({
    six.text_type: voluptuous.Any(UUID,
                                  MetricsController.Metric),
})
<commit_msg>duplicate UUID method<commit_after># -*- encoding: utf-8 -*-
#
# Copyright © 2014 eNovance
#
# Authors: <NAME> <<EMAIL>>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import functools
import json
import uuid

import iso8601
from oslo.utils import strutils
from oslo.utils import timeutils
import pecan
from pecan import rest
from pytimeparse import timeparse
import six
from six.moves.urllib import parse as urllib_parse
from stevedore import extension
import voluptuous
import webob.exc
import werkzeug.http

from gnocchi import aggregates
from gnocchi import archive_policy
from gnocchi import indexer
from gnocchi.openstack.common import policy
from gnocchi import storage

LOGICAL_AND = '∧'


_ENFORCER = None


def enforce(rule, target):
    """Return the user and project the request should be limited to.

    :param rule: The rule name
    :param target: The target to enforce on.

    """
    global _ENFORCER
    if not _ENFORCER:
        _ENFORCER = policy.Enforcer()

    headers = pecan.request.headers

    # NOTE(jd) If user_id or project_id are UUID, try to convert them in the
    # proper dashed format. It's indeed possible that a middleware passes
    # theses UUID without the dash representation. It's valid, we can parse,
    # but the policy module won't see the equality in the string
    # representations.

    user_id = headers.get("X-User-Id")
    try:
        user_id = six.text_type(uuid.UUID(user_id))
    except Exception:
        pass

    project_id = headers.get("X-Project-Id")
    try:
        project_id = six.text_type(uuid.UUID(project_id))
    except Exception:
        pass

    creds = {
        'roles': headers.get("X-Roles", "").split(","),
        'user_id': user_id,
        'project_id': project_id
    }

    if not _ENFORCER.enforce(rule, target, creds):
        pecan.abort(403)


def set_resp_location_hdr(location):
    # NOTE(sileht): according the pep-3333 the headers must be
    # str in py2 and py3 even this is not the same thing in both
    # version
    # see: http://legacy.python.org/dev/peps/pep-3333/#unicode-issues
    if six.PY2 and isinstance(location, six.text_type):
        location = location.encode('utf-8')
    location = urllib_parse.quote(location)
    pecan.response.headers['Location'] = location


def get_user_and_project():
    return (pecan.request.headers.get('X-User-Id'),
            pecan.request.headers.get('X-Project-Id'))


def deserialize(schema):
    mime_type, options = werkzeug.http.parse_options_header(
        pecan.request.headers.get('Content-Type'))
    if mime_type != "application/json":
        pecan.abort(415)
    try:
        params = json.loads(pecan.request.body.decode(
            options.get('charset', 'ascii')))
    except Exception as e:
        pecan.abort(400, "Unable to decode body: " + str(e))
    try:
        return schema(params)
    except voluptuous.Error as e:
        pecan.abort(400, "Invalid input: %s" % e)


def vexpose(schema, *vargs, **vkwargs):
    def expose(f):
        f = pecan.expose(*vargs, **vkwargs)(f)

        @functools.wraps(f)
        def callfunction(*args, **kwargs):
            return f(*args, body=deserialize(schema), **kwargs)
        return callfunction
    return expose


def Timestamp(v):
    if v is None:
        return v
    try:
        v = float(v)
    except (ValueError, TypeError):
        return timeutils.normalize_time(iso8601.parse_date(v))
    return datetime.datetime.utcfromtimestamp(v)


def convert_metric_list(metrics, created_by_user_id, created_by_project_id):
    # Replace an archive policy as value for an metric by a brand
    # a new metric
    new_metrics = {}
    for k, v in six.iteritems(metrics):
        if isinstance(v, uuid.UUID):
            new_metrics[k] = v
        else:
            new_metrics[k] = str(MetricsController.create_metric(
                created_by_user_id, created_by_project_id,
                v['archive_policy_name']))
    return new_metrics


def PositiveOrNullInt(value):
    value = int(value)
    if value < 0:
        raise ValueError("Value must be positive")
    return value


def PositiveNotNullInt(value):
    value = int(value)
    if value <= 0:
        raise ValueError("Value must be positive and not null")
    return value


def Timespan(value):
    if value is None:
        raise ValueError("Invalid timespan")
    try:
        seconds = int(value)
    except Exception:
        try:
            seconds = timeparse.timeparse(six.text_type(value))
        except Exception:
            raise ValueError("Unable to parse timespan")
    if seconds is None:
        raise ValueError("Unable to parse timespan")
    if seconds <= 0:
        raise ValueError("Timespan must be positive")
    return seconds


def get_details(params):
    type, options = werkzeug.http.parse_options_header(
        pecan.request.headers.get('Accept'))
    try:
        details = strutils.bool_from_string(
            options.get('details', params.pop('details', 'false')),
            strict=True)
    except ValueError as e:
        method = 'Accept' if 'details' in options else 'query'
        pecan.abort(
            400,
            "Unable to parse details value in %s: %s" % (method, str(e)))
    return details


class ArchivePoliciesController(rest.RestController):
    ArchivePolicy = voluptuous.Schema({
        voluptuous.Required("name"): six.text_type,
        voluptuous.Required("back_window", default=0): PositiveOrNullInt,
        voluptuous.Required("definition"):
        voluptuous.All([{
            "granularity": Timespan,
            "points": PositiveNotNullInt,
            "timespan": Timespan,
            }], voluptuous.Length(min=1)),
        })

    @staticmethod
    @vexpose(ArchivePolicy, 'json')
    def post(body):
        # Validate the data
        try:
            ap = archive_policy.ArchivePolicy.from_dict(body)
        except ValueError as e:
            pecan.abort(400, e)
        enforce("create archive policy", ap.to_dict())
        try:
            ap = pecan.request.indexer.create_archive_policy(ap)
        except indexer.ArchivePolicyAlreadyExists as e:
            pecan.abort(409, e)

        location = "/v1/archive_policy/" + ap['name']
        set_resp_location_hdr(location)
        pecan.response.status = 201
        return archive_policy.ArchivePolicy.from_dict(
            ap).to_human_readable_dict()

    @pecan.expose('json')
    def get_one(self, id):
        ap = pecan.request.indexer.get_archive_policy(id)
        if ap:
            enforce("get archive policy", ap)
            return archive_policy.ArchivePolicy.from_dict(
                ap).to_human_readable_dict()
        pecan.abort(404)

    @pecan.expose('json')
    def get_all(self):
        enforce("list archive policy", {})
        return [
            archive_policy.ArchivePolicy.from_dict(
                ap).to_human_readable_dict()
            for ap in pecan.request.indexer.list_archive_policies()
        ]

    @pecan.expose()
    def delete(self, name):
        try:
            pecan.request.indexer.delete_archive_policy(name)
        except indexer.NoSuchArchivePolicy as e:
            pecan.abort(404, e)
        except indexer.ArchivePolicyInUse as e:
            pecan.abort(400, e)


class AggregatedMetricController(rest.RestController):
    _custom_actions = {
        'measures': ['GET']
    }

    def __init__(self, metric_ids):
        self.metric_ids = metric_ids

    @pecan.expose('json')
    def get_measures(self, start=None, stop=None, aggregation='mean',
                     needed_overlap=100.0):
        if (aggregation
           not in archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS):
            pecan.abort(
                400,
                'Invalid aggregation value %s, must be one of %s'
                % (aggregation,
                   archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS))

        # Check RBAC policy
        metrics = pecan.request.indexer.get_metrics(self.metric_ids)
        missing_metric_ids = (set(m['id'] for m in metrics)
                              - set(self.metric_ids))
        if missing_metric_ids:
            # Return the first missing one in the error
            pecan.abort(404, storage.MetricDoesNotExist(
                missing_metric_ids[0]))

        for metric in metrics:
            enforce("get metric", metric)

        try:
            measures = pecan.request.storage.get_cross_metric_measures(
                self.metric_ids, start, stop, aggregation, needed_overlap)
            # Replace timestamp keys by their string versions
            return [(timeutils.strtime(timestamp), offset, v)
                    for timestamp, offset, v in measures]
        except storage.MetricUnaggregatable:
            pecan.abort(400, "One of the metric to aggregated doesn't have "
                        "matching granularity")
        except storage.MetricDoesNotExist as e:
            pecan.abort(404, str(e))


class MetricController(rest.RestController):
    _custom_actions = {
        'measures': ['POST', 'GET']
    }

    def __init__(self, metric_id):
        self.metric_id = metric_id
        mgr = extension.ExtensionManager(namespace='gnocchi.aggregates',
                                         invoke_on_load=True)
        self.custom_agg = dict((x.name, x.obj) for x in mgr)

    Measures = voluptuous.Schema([{
        voluptuous.Required("timestamp"):
        Timestamp,
        voluptuous.Required("value"): voluptuous.Any(float, int),
    }])

    def enforce_metric(self, rule):
        metrics = pecan.request.indexer.get_metrics((self.metric_id,))
        if not metrics:
            pecan.abort(404, storage.MetricDoesNotExist(self.metric_id))
        enforce(rule, metrics[0])

    @pecan.expose('json')
    def get_all(self, **kwargs):
        details = get_details(kwargs)
        metrics = pecan.request.indexer.get_metrics((self.metric_id,),
                                                    details=details)
        if not metrics:
            pecan.abort(404, storage.MetricDoesNotExist(self.metric_id))

        metric = metrics[0]
        enforce("get metric", metric)

        if details:
            metric['archive_policy'] = (
                archive_policy.ArchivePolicy.from_dict(
                    metric['archive_policy']
                ).to_human_readable_dict())
        return metric

    @vexpose(Measures)
    def post_measures(self, body):
        self.enforce_metric("post measures")
        try:
            pecan.request.storage.add_measures(
                self.metric_id,
                (storage.Measure(
                    m['timestamp'],
                    m['value']) for m in body))
        except storage.MetricDoesNotExist as e:
            pecan.abort(404, str(e))
        except storage.NoDeloreanAvailable as e:
            pecan.abort(400,
                        "The measure for %s is too old considering the "
                        "archive policy used by this metric. "
                        "It can only go back to %s."
                        % (e.bad_timestamp, e.first_timestamp))

    @pecan.expose('json')
    def get_measures(self, start=None, stop=None, aggregation='mean', **param):
        self.enforce_metric("get measures")
        if not (aggregation
                in archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS
                or aggregation in self.custom_agg):
            msg = '''Invalid aggregation value %(agg)s, must be one of %(std)s
                     or %(custom)s'''
            pecan.abort(400, msg % dict(
                agg=aggregation,
                std=archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS,
                custom=str(self.custom_agg.keys())))

        if start is not None:
            try:
                start = Timestamp(start)
            except Exception:
                pecan.abort(400, "Invalid value for start")

        if stop is not None:
            try:
                stop = Timestamp(stop)
            except Exception:
                pecan.abort(400, "Invalid value for stop")

        try:
            if aggregation in self.custom_agg:
                measures = self.custom_agg[aggregation].compute(
                    pecan.request.storage, self.metric_id, start, stop,
                    **param)
            else:
                measures = pecan.request.storage.get_measures(self.metric_id,
                                                              start, stop,
                                                              aggregation)
            # Replace timestamp keys by their string versions
            return [(timeutils.strtime(timestamp), offset, v)
                    for timestamp, offset, v in measures]
        except storage.MetricDoesNotExist as e:
            pecan.abort(404, str(e))
        except aggregates.CustomAggFailure as e:
            pecan.abort(400, str(e))

    @pecan.expose()
    def delete(self):
        self.enforce_metric("delete metric")
        try:
            pecan.request.storage.delete_metric(self.metric_id)
        except storage.MetricDoesNotExist as e:
            pecan.abort(404, str(e))
        pecan.request.indexer.delete_metric(self.metric_id)


def UUID(value):
    try:
        return uuid.UUID(value)
    except Exception as e:
        raise ValueError(e)


MetricSchemaDefinition = {
    "user_id": UUID,
    "project_id": UUID,
    voluptuous.Required('archive_policy_name'): six.text_type,
}


class MetricsController(rest.RestController):
    @staticmethod
    @pecan.expose()
    def _lookup(id, *remainder):
        # That's triggered when accessing /v1/metric/
        if id is "":
            pecan.abort(404)
        return MetricController(id), remainder

    Metric = voluptuous.Schema(MetricSchemaDefinition)

    @staticmethod
    def create_metric(created_by_user_id, created_by_project_id,
                      archive_policy_name,
                      user_id=None, project_id=None):
        enforce("create metric", {
            "created_by_user_id": created_by_user_id,
            "created_by_project_id": created_by_project_id,
            "user_id": user_id,
            "project_id": project_id,
            "archive_policy_name": archive_policy_name,
        })
        id = uuid.uuid4()
        policy = pecan.request.indexer.get_archive_policy(archive_policy_name)
        if policy is None:
            pecan.abort(400, "Unknown archive policy %s" % archive_policy_name)
        ap = archive_policy.ArchivePolicy.from_dict(policy)
        pecan.request.indexer.create_metric(
            id,
            created_by_user_id, created_by_project_id,
            archive_policy_name=policy['name'])
        pecan.request.storage.create_metric(str(id), ap)
        return id

    @vexpose(Metric, 'json')
    def post(self, body):
        user, project = get_user_and_project()
        id = self.create_metric(user, project, **body)
        set_resp_location_hdr("/v1/metric/" + str(id))
        pecan.response.status = 201
        return {"id": str(id),
                "archive_policy_name": str(body['archive_policy_name'])}

    @pecan.expose('json')
    def get_all(self, **kwargs):
        try:
            enforce("list all metric", {})
        except webob.exc.HTTPForbidden:
            enforce("list metric", {})
            user_id, project_id = get_user_and_project()
            provided_user_id = kwargs.get('user_id')
            provided_project_id = kwargs.get('project_id')
            if ((provided_user_id and user_id != provided_user_id)
               or (provided_project_id and project_id != provided_project_id)):
                pecan.abort(
                    403, "Insufficient privileges to filter by user/project")
        else:
            user_id = kwargs.get('user_id')
            project_id = kwargs.get('project_id')
        return pecan.request.indexer.list_metrics(
            user_id, project_id)


Metrics = voluptuous.Schema({
    six.text_type: voluptuous.Any(UUID,
                                  MetricsController.Metric),
})

{'fraction_non_alphanumeric': 0.09641517924103794, 'fraction_numerical': 0.00791960401979901, 'mean_word_length': 2.227417949462678, 'pattern_counts': {'":': 28, '<': 15, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 18, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2725603', 'n_tokens_mistral': 9859, 'n_tokens_neox': 9500, 'n_words': 2321}