diff --git a/run.py b/run.py --- a/run.py +++ b/run.py @@ -1,3 +1,3 @@ -from spaceapi import app +from spaceapi import create_app -app.run(debug=True) +create_app().run(debug=True) diff --git a/spaceapi/__init__.py b/spaceapi/__init__.py --- a/spaceapi/__init__.py +++ b/spaceapi/__init__.py @@ -1,210 +1,52 @@ import json import os import os.path -import calendar import base64 -import copy - -from time import time -from ast import literal_eval - -from flask import Flask, jsonify, request, render_template -from flask.ext.httpauth import HTTPDigestAuth - -import jsonschema -from pkg_resources import resource_filename - -ALLOWED_STATE_KEYS = { - 'open': bool, - 'lastchange': int, - 'trigger_person': str, - 'message': str -} - -ALLOWED_SENSORS_KEYS = json.load( - open(resource_filename('spaceapi', 'schema/sensors.json'), - encoding='utf-8') -) +from flask import Flask config_file = os.path.abspath('config.json') -default_json_file = os.path.abspath('default.json') -last_state_file = os.path.abspath('laststate.json') - -default_json = {} -active_json = {} - - -def reload_json(): - global default_json - global active_json - - default_json = json.load(open(default_json_file, encoding='utf-8')) - - if os.path.exists(last_state_file): - with open(last_state_file, encoding='utf-8') as f: - active_json = json.load(f) - if os.path.getmtime(last_state_file) \ - < os.path.getmtime(default_json_file): - backup = copy.deepcopy(active_json) - active_json.update(default_json) - active_json['state']['open'] = backup['state']['open'] - active_json['state']['lastchange'] = backup['state']['lastchange'] - else: - active_json = copy.deepcopy(default_json) - -reload_json() +def create_app(): + app = Flask(__name__) -app = Flask(__name__) -auth = HTTPDigestAuth() - -_default_secret_key = base64.b64encode(os.urandom(32)).decode('utf-8') -app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', _default_secret_key) - -if not hasattr(app.config, 'from_json'): - def from_json(file, silent=True): - try: - with open(file, encoding='utf-8') as json_file: - obj = json.load(json_file) - except IOError: - if silent: - return False - raise + _default_secret_key = base64.b64encode(os.urandom(32)).decode('utf-8') + app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', _default_secret_key) - for key in obj: - if key.isupper(): - app.config[key] = obj[key] - - return True - - app.config.from_json = from_json - -app.config.from_json(config_file, silent=True) - - -@auth.get_password -def get_pw(username): - if username == app.config.get('HTTP_DIGEST_AUTH_USER'): - return app.config.get('HTTP_DIGEST_AUTH_PASSWORD') - return None - + if not hasattr(app.config, 'from_json'): + def from_json(file, silent=True): + try: + with open(file, encoding='utf-8') as json_file: + obj = json.load(json_file) + except IOError: + if silent: + return False + raise -def request_wants_json(): - best = request.accept_mimetypes.best_match( - ['application/json', 'text/html'] - ) - return best == 'application/json' and \ - request.accept_mimetypes[best] > \ - request.accept_mimetypes['text/html'] - - -def save_last_state(): - - with open(last_state_file, mode='w', encoding='utf-8') as f: - json.dump(active_json, f, sort_keys=True) - - -@app.route('/') -def index(): - - if request_wants_json(): - return jsonify(active_json) - return render_template('index.html', status=active_json) - - -@app.route('/status.json') -def status_json(): - return jsonify(active_json) - + for key in obj: + if key.isupper(): + app.config[key] = obj[key] -@app.route('/set/state//', methods=['POST']) -@auth.login_required -def set_state(key, value): - - value = literal_eval(value) - - if key in ALLOWED_STATE_KEYS and isinstance(value, - ALLOWED_STATE_KEYS[key]): - active_json['state'][key] = value + return True - if key == 'open': - active_json['state']['lastchange'] = int(time.time()) - else: - return 400 - - save_last_state() - - return jsonify(active_json) - + app.config.from_json = from_json -def fuzzy_list_find(lst, key, value): - - for i, dic in enumerate(lst): - if dic[key] == value: - return i - - raise ValueError - + app.config.from_json(config_file, silent=True) -@app.route('/set/sensors/', methods=['POST']) -@auth.login_required -def set_sensors(key): - - if key in ALLOWED_SENSORS_KEYS and key in active_json['sensors']: - data = request.data - try: - data = json.loads(data) - try: - jsonschema.validate(data, ALLOWED_SENSORS_KEYS[key]) + @app.after_request + def add_headers(response): + response.headers.setdefault('Access-Control-Allow-Origin', '*') + response.headers.setdefault('Cache-Control', 'no-cache') - if key != 'radiation': - for subkey in ('name', 'location'): - if subkey in data: - index = fuzzy_list_find( - active_json['sensors'][key], - subkey, data[subkey] - ) - active_json['sensors'][key][index].update(data) - else: - return 400 - else: - for first_subkey in ('alpha', 'beta', - 'gamma' 'beta_gamma'): - for second_subkey in ('name', 'location'): - if second_subkey in data[first_subkey]: - index = fuzzy_list_find( - active_json['sensors'][key][first_subkey], - second_subkey, - data[first_subkey][second_subkey]) - active_json['sensors'][ - key - ][first_subkey][index].update(data) - else: - return 400 + return response + + from .views import root_views + app.register_blueprint(root_views) - except jsonschema.ValidationError: - return 400 - except: - return 400 - else: - return 400 - - save_last_state() - - return jsonify(active_json) - + from .state import state_views + app.register_blueprint(state_views, url_prefix='/state') -@app.route('/reload') -@auth.login_required -def reload(): - reload_json() - return 200 - + from .sensors import sensors_views + app.register_blueprint(sensors_views, url_prefix='/sensors') -@app.after_request -def add_headers(response): - response.headers.setdefault('Access-Control-Allow-Origin', '*') - response.headers.setdefault('Cache-Control', 'no-cache') - - return response + return app diff --git a/spaceapi/active.py b/spaceapi/active.py new file mode 100644 --- /dev/null +++ b/spaceapi/active.py @@ -0,0 +1,36 @@ +import copy +import json +import os.path + + +default_json_file = os.path.abspath('default.json') +last_state_file = os.path.abspath('laststate.json') + +default_json = {} +active_json = {} + +def reload_json(): + global default_json + global active_json + + default_json = json.load(open(default_json_file, encoding='utf-8')) + + if os.path.exists(last_state_file): + with open(last_state_file, encoding='utf-8') as f: + active_json = json.load(f) + + if os.path.getmtime(last_state_file) \ + < os.path.getmtime(default_json_file): + backup = copy.deepcopy(active_json) + active_json.update(default_json) + active_json['state']['open'] = backup['state']['open'] + active_json['state']['lastchange'] = backup['state']['lastchange'] + else: + active_json = copy.deepcopy(default_json) + +reload_json() + +def save_last_state(): + + with open(last_state_file, mode='w', encoding='utf-8') as f: + json.dump(active_json, f, sort_keys=True) diff --git a/spaceapi/auth.py b/spaceapi/auth.py new file mode 100644 --- /dev/null +++ b/spaceapi/auth.py @@ -0,0 +1,9 @@ +from flask.ext.httpauth import HTTPDigestAuth + +httpauth = HTTPDigestAuth() + +@httpauth.get_password +def get_pw(username): + if username == app.config.get('HTTP_DIGEST_AUTH_USER'): + return app.config.get('HTTP_DIGEST_AUTH_PASSWORD') + return None diff --git a/spaceapi/sensors.py b/spaceapi/sensors.py new file mode 100644 --- /dev/null +++ b/spaceapi/sensors.py @@ -0,0 +1,96 @@ +import json + +from pkg_resources import resource_filename + +import jsonschema +from flask import Blueprint, jsonify + +from .auth import httpauth +from .active import active_json, save_last_state +from .utils import first, fuzzy_list_find + +sensors_views = Blueprint('sensors', __name__) + +ALLOWED_SENSORS_KEYS = json.load( + open(resource_filename('spaceapi', 'schema/sensors.json'), + encoding='utf-8') +) + +RADIATON_SUBKEYS = ('alpha', 'beta', 'gamma', 'beta_gamma') + +IDENTIFICATION_KEYS = ('name', 'location') + + +def get_identification_key(data): + return first(data, IDENTIFICATION_KEYS) + + +def set_value(data, key): + + try: + subkey = get_identification_key(data) + except ValueError: + return 400 + + try: + index = fuzzy_list_find(active_json['sensors'][key], + key, data[subkey]) + active_json['sensors'][key][index].update(data) + except ValueError: + active_json['sensors'][key].append(data) + + save_last_state() + return jsonify(active_json) + + +def set_radiation_value(data): + + radiation_keys = [k for k in RADIATON_SUBKEYS if k in data] + + if not radiation_keys: + return 400 + + for first_subkey in radiation_keys: + + try: + second_subkey = get_identification_key(data[first_subkey]) + except ValueError: + return 400 + + try: + index = fuzzy_list_find( + active_json['sensors']['radiation'][first_subkey], + second_subkey, + data[first_subkey][second_subkey]) + active_json['sensors'][ + 'radiation' + ][first_subkey][index].update(data) + except ValueError: + active_json['sensors']['radiaton'][first_subkey].append(data) + + + save_last_state() + return jsonify(active_json) + +@sensors_views.route('/set/', methods=['POST']) +@httpauth.login_required +def set_sensors(key): + + if key in ALLOWED_SENSORS_KEYS and key in active_json['sensors']: + data = request.data + try: + data = json.loads(data) + + try: + jsonschema.validate(data, ALLOWED_SENSORS_KEYS[key]) + except jsonschema.ValidationError: + return 400 + + if key != 'radiation': + return set_value(data, key) + else: + return set_radiation_value(data) + except ValueError: + return 400 + else: + return 400 diff --git a/spaceapi/state.py b/spaceapi/state.py new file mode 100644 --- /dev/null +++ b/spaceapi/state.py @@ -0,0 +1,35 @@ +from time import time + +from flask import Blueprint, jsonify, request + +from .active import active_json, save_last_state +from .auth import httpauth + +state_views = Blueprint('state', __name__) + +ALLOWED_STATE_KEYS = { + 'open': bool, + 'lastchange': int, + 'trigger_person': str, + 'message': str +} + +@state_views.route('/set/', methods=['POST']) +@httpauth.login_required +def set_state(key): + + value = request.form['value'] + value = literal_eval(value) + + if key in ALLOWED_STATE_KEYS and isinstance(value, + ALLOWED_STATE_KEYS[key]): + active_json['state'][key] = value + + if key == 'open': + active_json['state']['lastchange'] = int(time()) + else: + return 400 + + save_last_state() + + return jsonify(active_json) diff --git a/spaceapi/utils.py b/spaceapi/utils.py new file mode 100644 --- /dev/null +++ b/spaceapi/utils.py @@ -0,0 +1,25 @@ +from flask import request + +def request_wants_json(): + best = request.accept_mimetypes.best_match( + ['application/json', 'text/html'] + ) + return best == 'application/json' and \ + request.accept_mimetypes[best] > \ + request.accept_mimetypes['text/html'] + + +def fuzzy_list_find(lst, key, value): + + for i, dic in enumerate(lst): + if dic[key] == value: + return i + + raise ValueError + +def first(iterable, keys): + for el in keys: + if el in iterable: + return el + + raise ValueError diff --git a/spaceapi/views.py b/spaceapi/views.py new file mode 100644 --- /dev/null +++ b/spaceapi/views.py @@ -0,0 +1,23 @@ +from flask import Blueprint, jsonify, render_template + +from .auth import httpauth +from .active import active_json, reload_json +from .utils import request_wants_json + +root_views = Blueprint('root', __name__) + +@root_views.route('/') +def index(): + if request_wants_json(): + return jsonify(active_json) + return render_template('index.html', status=active_json) + +@root_views.route('/status.json') +def status_json(): + return jsonify(active_json) + +@root_views.route('/reload') +@httpauth.login_required +def reload(): + reload_json() + return 200