-
David Hoese authoredDavid Hoese authored
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)