Skip to content
Snippets Groups Projects
server.py 5.85 KiB
import json as builtin_json
import logging
import os
import sys
from datetime import datetime
from enum import Enum
from urllib.error import URLError
from urllib.request import urlopen

from flask import Flask, jsonify, render_template, request
from flask_cors import CORS
from flask_json import FlaskJSON

from metobsapi import data_api, files_api
from metobsapi.util import data_responses, file_responses

LOG = logging.getLogger(__name__)

app = Flask(__name__)

# Load custom configuration file is specified
app.config.from_object("metobsapi.common_config")
if os.environ.get("METOBSAPI_SETTINGS") is not None:
    app.config.from_pyfile(os.environ.get("METOBSAPI_SETTINGS"))
app.config.from_prefixed_env(prefix="METOBSAPI")


# Load json handler and add custom enum encoder
json = FlaskJSON(app)


@json.encoder
def enum_encoder(o):
    if isinstance(o, Enum):
        return o.value


# Allow for cross-domain access to the API using CORS
CORS(app, resources=r"/api/*", allow_headers="Content-Type")


@app.route("/api/")
def index():
    """Main App Documentation"""
    return render_template("index.html")


@app.route("/api/files")
def files_index():
    """Files API Documentation"""
    return render_template(
        "files_index.html",
        archive_info=file_responses.ARCHIVE_STREAMS,
        instrument_streams=file_responses.INSTRUMENT_STREAMS,
    )


@app.route("/api/data")
def data_index():
    """Data API Documentation"""
    return render_template("data_index.html")


@app.errorhandler(404)
def page_not_found(e):
    return render_template("404.html"), 404


@app.errorhandler(500)
def internal_server(e):
    return render_template("500.html"), 500


@app.after_request
def apply_header(response):
    response.headers[data_responses.api_version_header] = data_responses.api_version
    return response


@app.route("/api/data.<fmt>", methods=["GET"])
def get_data(fmt):
    begin_time = request.args.get("begin")
    end_time = request.args.get("end")
    site = request.args.get("site")
    inst = request.args.get("inst")
    symbols = request.args.get("symbols")
    interval = request.args.get("interval")
    sep = request.args.get("sep", ",")
    order = request.args.get("order", "row")
    epoch = request.args.get("epoch")

    result = data_api.modify_data(fmt, begin_time, end_time, site, inst, symbols, interval, sep, order, epoch)

    return result


@app.route("/api/files.<fmt>", methods=["GET"])
def get_files(fmt):
    begin_time = request.args.get("begin")
    end_time = request.args.get("end")
    dates = request.args.get("dates")
    streams = request.args.get("streams")
    return files_api.find_stream_files(fmt, begin_time, end_time, dates, streams)


@app.route("/api/archive/info", methods=["GET"])
def get_archive_info():
    return jsonify(
        {
            "code": 200,
            "message": "",
            "sites": file_responses.ARCHIVE_INFO,
        }
    )


@app.route("/api/status", methods=["GET"])
def status_index():
    return render_template("status_index.html")


def _status_dict_to_html(response):
    items = "<br>\n".join("{}: {}".format(k, v) for k, v in sorted(response.items()))
    return """<html>
<body>
{}
</body>
</html>""".format(
        items
    )


def _status_render(response, fmt):
    if fmt == "json":
        return jsonify(response)
    else:
        return _status_dict_to_html(response)


@app.route("/api/status/<site>/<inst>.<fmt>", methods=["GET"])
@app.route("/api/status/<site>/<inst>", methods=["GET"])
@app.route("/api/status/<site>.<fmt>", methods=["GET"])
@app.route("/api/status/<site>", methods=["GET"])
def get_instrument_status(site, inst=None, fmt=None):
    """See `/api/status/` for more information."""
    # defaults:
    response = {
        "name": site if inst is None else inst,
        "short_name": "",
        "long_name": "",
        "status_code": 255,
        "status_message": "",
    }
    mod_time = datetime.utcnow()

    if fmt is None:
        fmt = "html"
    if fmt not in ["html", "json"]:
        return render_template("400.html", format=fmt), 400

    if inst is None:
        json_subpath = os.path.join(site, "status.json")
    else:
        json_subpath = os.path.join(site, inst, "status.json")

    # try to load the JSON file from the archive
    if not os.path.isfile(app.config.get("ARCHIVE_ROOT")) and app.config.get("ARCHIVE_ROOT").startswith("http"):
        LOG.warning("Using URL request for status JSON, not meant for operational use")
        # we aren't on a system with the archive available, fall back to URL
        # loads directly to the archive
        base_url = app.config.get("ARCHIVE_URL")
        json_url = os.path.join(base_url, json_subpath)
        try:
            # Security: We check to ensure this is an HTTP URL as a base URL.
            #   The server configuration is also the one setting what the root URL is.
            json_str = urlopen(json_url).read()  # nosec B310
        except URLError:
            response["status_message"] = "Could not retrieve configured status: {}".format(json_url)
            json_str = None
    else:
        base_path = app.config.get("ARCHIVE_ROOT")
        json_path = os.path.join(base_path, json_subpath)
        try:
            json_str = open(json_path, "r").read()
            mod_time = datetime.fromtimestamp(os.path.getmtime(json_path))
        except FileNotFoundError:
            response["status_message"] = "No status information found."
            json_str = None

    if json_str is None:
        # exit out early, we don't know what this is
        return _status_render(response, fmt)

    json_dict = builtin_json.loads(json_str)
    response["last_updated"] = mod_time.strftime("%Y-%m-%d %H:%M:%SZ")
    response.update(json_dict)
    return _status_render(response, fmt)


if __name__ == "__main__":
    app.debug = True
    bind_addr = "0.0.0.0" if len(sys.argv) <= 1 else sys.argv[0]  # nosec B104
    app.run(bind_addr, threaded=True)