Adds search and retrieval routes to a :class:`meteorpi_server.MeteorServer` instance

    :param MeteorApp meteor_app:
        The :class:`meteorpi_server.MeteorApp` to which routes should be added
    :param string url_path:
        The base path used for the query routes, defaults to ''

def add_routes(meteor_app, url_path=''):
    """
    Adds search and retrieval routes to a :class:`meteorpi_server.MeteorServer` instance

    :param MeteorApp meteor_app:
        The :class:`meteorpi_server.MeteorApp` to which routes should be added
    :param string url_path:
        The base path used for the query routes, defaults to ''
    """
    from meteorpi_server import MeteorApp

    app = meteor_app.app

    @app.after_request
    def after_request(response):
        response.headers.add('Accept-Ranges', 'bytes')
        return response

    # Return a list of all of the observatories which are registered in this repository
    # A dictionary of basic information is returned for each
    @app.route('{0}/obstories'.format(url_path), methods=['GET'])
    def get_obstories():
        db = meteor_app.get_db()
        obstories = db.get_obstory_ids()
        output = {}
        for o in obstories:
            output[o] = db.get_obstory_from_id(o)
            db.con.execute("SELECT m.time FROM archive_metadata m "
                           "INNER JOIN archive_observatories l ON m.observatory = l.uid "
                           "AND l.publicId = %s AND m.time>0 "
                           "ORDER BY m.time ASC LIMIT 1",
                           (o,))
            first_seen = 0
            results = db.con.fetchall()
            if results:
                first_seen = results[0]['time']
            db.con.execute("SELECT m.time FROM archive_metadata m "
                           "INNER JOIN archive_observatories l ON m.observatory = l.uid "
                           "AND l.publicId = %s AND m.time>0 "
                           "ORDER BY m.time DESC LIMIT 1",
                           (o,))
            last_seen = 0
            results = db.con.fetchall()
            if results:
                last_seen = results[0]['time']
            output[o]['firstSeen'] = first_seen
            output[o]['lastSeen'] = last_seen
        db.close_db()
        return jsonify(output)

    # Return a list of all of the metadata tags which ever been set on a particular observatory, with time stamp
    @app.route('{0}/obstory/<obstory_id>/metadata'.format(url_path), methods=['GET'])
    def get_obstory_status_all(obstory_id):
        db = meteor_app.get_db()
        search = mp.ObservatoryMetadataSearch(obstory_ids=[obstory_id], time_min=0, time_max=time.time())
        data = db.search_obstory_metadata(search)['items']
        data.sort(key=lambda x: x.time)
        output = [[i.time, i.key, i.value] for i in data]
        db.close_db()
        return jsonify({'status': output})

    # Return a list of all of the metadata which was valid for a particular observatory at a particular time
    @app.route('{0}/obstory/<obstory_id>/statusdict'.format(url_path), methods=['GET'])
    @app.route('{0}/obstory/<obstory_id>/statusdict/<unix_time>'.format(url_path), methods=['GET'])
    def get_obstory_status_by_time(obstory_id, unix_time=None):
        db = meteor_app.get_db()
        if unix_time is None:
            unix_time = time.time()
        status = {}
        try:
            obstory_info = db.get_obstory_from_id(obstory_id)
            if obstory_info:
                obstory_name = obstory_info['name']
                status = db.get_obstory_status(obstory_name=obstory_name, time=float(unix_time))
        except ValueError:
            return jsonify({'error': 'No such observatory "%s".' % obstory_id})
        db.close_db()
        return jsonify({'status': status})

    # Search for observations using a YAML search string
    @app.route('{0}/obs/<search_string>'.format(url_path), methods=['GET'], strict_slashes=True)
    def search_events(search_string):
        db = meteor_app.get_db()
        try:
            search = mp.ObservationSearch.from_dict(safe_load(unquote(search_string)))
        except ValueError:
            return jsonify({'error': str(sys.exc_info()[1])})
        observations = db.search_observations(search)
        db.close_db()
        return jsonify({'obs': list(x.as_dict() for x in observations['obs']), 'count': observations['count']})

    # Search for files using a YAML search string
    @app.route('{0}/files/<search_string>'.format(url_path), methods=['GET'])
    def search_files(search_string):
        db = meteor_app.get_db()
        try:
            search = mp.FileRecordSearch.from_dict(safe_load(unquote(search_string)))
        except ValueError:
            return jsonify({'error': str(sys.exc_info()[1])})
        files = db.search_files(search)
        db.close_db()
        return jsonify({'files': list(x.as_dict() for x in files['files']), 'count': files['count']})

    # Return a list of sky clarity measurements for a particular observatory (scale 0-100)
    @app.route('{0}/skyclarity/<obstory_id>/<utc_min>/<utc_max>/<period>'.format(url_path), methods=['GET'])
    def get_skyclarity(obstory_id, utc_min, utc_max, period):
        db = meteor_app.get_db()
        utc_min = float(utc_min)
        utc_max = float(utc_max)
        period = float(period)
        count = 0
        output = []
        while count < 250:
            a = utc_min + period * count
            b = a + period
            count += 1
            db.con.execute("SELECT m.floatValue FROM archive_metadata m "
                           "INNER JOIN archive_files f ON m.fileId = f.uid "
                           "INNER JOIN archive_semanticTypes fs ON f.semanticType = fs.uid "
                           "INNER JOIN archive_metadataFields mf ON m.fieldId = mf.uid "
                           "INNER JOIN archive_observations o ON f.observationId = o.uid "
                           "INNER JOIN archive_observatories l ON o.observatory = l.uid "
                           "WHERE mf.metaKey='meteorpi:skyClarity' "
                           "AND l.publicId = %s "
                           "AND fs.name='meteorpi:timelapse/frame/bgrdSub/lensCorr' "
                           "AND f.fileTime>%s AND f.fileTime<%s "
                           "LIMIT 250",
                           (obstory_id, a, b))
            results = db.con.fetchall()
            if len(results) > 0:
                output.append(sum([i['floatValue'] for i in results]) / len(results))
            else:
                output.append(0)
            if b >= utc_max:
                break
        db.close_db()
        return jsonify(output)

    # Return a list of the number of observations of a particular type in a sequence
    # of time intervals between utc_min and utc_max, with step size period
    @app.route('{0}/activity/<obstory_id>/<semantic_type>/<utc_min>/<utc_max>/<period>'.format(url_path),
               methods=['GET'])
    def get_activity(obstory_id, semantic_type, utc_min, utc_max, period):
        db = meteor_app.get_db()
        utc_min = float(utc_min)
        utc_max = float(utc_max)
        period = float(period)
        count = 0
        output = []
        while count < 250:
            a = utc_min + period * count
            b = a + period
            count += 1
            db.con.execute("SELECT COUNT(*) FROM archive_observations o "
                           "INNER JOIN archive_observatories l ON o.observatory = l.uid "
                           "INNER JOIN archive_semanticTypes s ON o.obsType = s.uid "
                           "WHERE l.publicId=%s AND s.name=%s AND o.obsTime>=%s AND o.obsTime<%s LIMIT 1",
                           (obstory_id, semantic_type, a, b))
            output.append(db.con.fetchone()['COUNT(*)'])
            if b >= utc_max:
                break
        db.close_db()
        return jsonify({"activity": output})

    # Return a thumbnail version of an image
    @app.route('{0}/thumbnail/<file_id>/<file_name>'.format(url_path), methods=['GET'])
    def get_thumbnail(file_id, file_name):
        db = meteor_app.get_db()
        record = db.get_file(repository_fname=file_id)
        if record is None:
            db.close_db()
            return MeteorApp.not_found(entity_id=file_id)
        if record.mime_type != "image/png":
            db.close_db()
            return MeteorApp.not_found(entity_id=file_id)
        file_path = db.file_path_for_id(record.id)
        thumb_path = os.path.join(db.file_store_path, "../thumbnails", record.id)
        if not os.path.exists(thumb_path):
            resize_tool = os.path.join(meteor_app.binary_path, "resize")
            os.system("%s %s 220 %s" % (resize_tool, file_path, thumb_path))
        db.close_db()
        return send_file(filename_or_fp=thumb_path, mimetype=record.mime_type)

    # Return a file from the repository
    @app.route('{0}/files/content/<file_id>/<file_name>'.format(url_path), methods=['GET'])
    @app.route('{0}/files/content/<file_id>'.format(url_path), methods=['GET'])
    def get_file_content(file_id, file_name=None):

        # http://blog.asgaard.co.uk/2012/08/03/http-206-partial-content-for-flask-python
        def send_file_partial(filename_or_fp, mimetype):
            range_header = request.headers.get('Range', None)
            if not range_header:
                return send_file(filename_or_fp, mimetype=record.mime_type)

            size = os.path.getsize(filename_or_fp)
            byte1, byte2 = 0, None

            m = re.search('(\d+)-(\d*)', range_header)
            g = m.groups()

            if g[0]:
                byte1 = int(g[0])
            if g[1]:
                byte2 = int(g[1])

            length = size - byte1
            if byte2 is not None:
                length = byte2 - byte1 + 1

            with open(filename_or_fp, 'rb') as f:
                f.seek(byte1)
                data = f.read(length)

            rv = Response(data,
                          206,
                          mimetype=mimetype,
                          direct_passthrough=True)
            rv.headers.add('Content-Range', 'bytes {0}-{1}/{2}'.format(byte1, byte1 + length - 1, size))

            return rv

        db = meteor_app.get_db()
        record = db.get_file(repository_fname=file_id)
        if record is None:
            db.close_db()
            return MeteorApp.not_found(entity_id=file_id)
        file_path = db.file_path_for_id(record.id)
        db.close_db()
        return send_file_partial(filename_or_fp=file_path, mimetype=record.mime_type)