def housekeeping_aggregate(function): """Aggregated PATE Housekeeping data GET /api/housekeeping/<string:function> Allowed aggregate functions are: avg, sum, min, max and count. Query parameters: begin - PATE timestamp (Unix timestamp) end - PATE timestamp (Unix timestamp) fields - A comma separated list of fields to return API returns 200 OK and: { ..., "data" : { <fields according to query parameter 'fields'> }, ... } Parameters 'begin' and 'end' are integers, although the 'timestamp' field they are compared to, is a decimal number. NOTE: This datetime format is placeholder, because instrument development has not formally specified the one used in the actual satellite. Internally, Python timestamp is used. Unlike in the above described API endpoint, these responses do not explicitly include primary key field ('timestamp'), because that would defeat the purpose of the aggregate functions. """ log_request(request) try: from api.Housekeeping import Housekeeping if function.lower() not in ('avg', 'sum', 'min', 'max', 'count'): raise api.InvalidArgument( "Function '{}' is not supported!".format(function)) return api.response(Housekeeping(request).get(function)) except Exception as e: return api.exception_response(e)
def housekeeping(): """PATE Housekeeping data GET /api/housekeeping Query parameters: begin - PATE timestamp (Unix timestamp) end - PATE timestamp (Unix timestamp) fields - A comma separated list of fields to return API returns 200 OK and: { ..., "data" : [ { <fields according to query parameter 'fields'> }, ... ], ... } Parameters 'begin' and 'end' are integers, although the 'rotation' field they are compared to, is a decimal number. NOTE: This datetime format is placeholder, because instrument development has not formally specified the one used in the actual satellite. Internally, Python timestamp is used. A JSON list of objects is returned. Among object properties, primary key 'timestamp' is always included, regardless what 'fields' argument specifies. Data exceeding 7 days should not be requested. For more data, CSV services should be used.""" log_request(request) try: from api.Housekeeping import Housekeeping return api.response(Housekeeping(request).get()) except Exception as e: return api.exception_response(e)
def hitcount_aggregate(function): """Aggregated classified PATE particle hits GET /api/hitcount/<string:function> Allowed aggregate functions are: avg, sum, min, max and count. Query parameters: begin - PATE timestamp (Unix timestamp) end - PATE timestamp (Unix timestamp) fields - A comma separated list of fields to return API returns 200 OK and: { ..., "data" : { <fields according to query parameter 'fields'> }, ... } Data is logically grouped into full rotations, each identified by the timestamp when the rotation started. Information on rotational period or starting time of each sector is not available within data. It must be deciphered separately, if needed. Parameters 'begin' and 'end' are integers, although the 'rotation' field they are compared to, is a decimal number. NOTE: This datetime format is placeholder, because instrument development has not formally specified the one used in the actual satellite. Internally, Python timestamp is used. A JSON list containing a single object is returned. The identifier field ('timestamp') is never included, because that would defeat the purpose of the aggregate functions.""" log_request(request) try: from api.HitCount import HitCount if function.lower() not in ('avg', 'sum', 'min', 'max', 'count'): raise api.InvalidArgument( "Function '{}' is not supported!".format(function)) return api.response(HitCount(request).get(function)) except Exception as e: return api.exception_response(e)
def hitcount(): """Classified PATE hit counters GET /api/hitcount Query parameters: begin - PATE timestamp (Unix timestamp) end - PATE timestamp (Unix timestamp) fields - A comma separated list of fields to return API returns 200 OK and: { ..., "data" : [ { <fields according to query parameter 'fields'> }, ... ], ... } Data is logically grouped into full rotations, each identified by the timestamp when the rotation started. Field/column descriptions are unavailable until they have been formally specified by instrument development. Parameters 'begin' and 'end' are integers, although the 'rotation' field they are compared to, is a decimal number. NOTE: This datetime format is placeholder, because instrument development has not formally specified the one used in the actual satellite. Internally, Python timestamp is used. A JSON list of objects is returned. Among object properties, primary key 'timestamp' is always included, regardless what 'fields' argument specifies. Data exceeding 7 days should not be requested. For more data, CSV services should be used.""" log_request(request) try: from api.HitCount import HitCount return api.response(HitCount(request).get()) except Exception as e: return api.exception_response(e)
def psu(): """Read PSU values. GET /api/psu No query parameters supported. API returns 200 OK and: { ..., "data" : { "power" : ("OFF" | "ON"), "state" : ("OK" | "OVER CURRENT"), "measured_current" : (float), "measured_voltage" : (float), "voltage_setting" : (float), "current_limit" : (float), "modified" : (int) }, ... } If the backend is not running, 404 Not Found is returned. """ log_request(request) try: from api.PSU import PSU return api.response(PSU(request).get()) except Exception as e: return api.exception_response(e)
def api_file(ftype=None): """List of downloadable file, with optional type filtering. Allowed types are "vm" and "usb". GET /api/file GET /api/file/usb GET /api/file/vm Query parameters: (none implemented) API returns 200 OK and: { ..., "data" : [ { TBA }, ... ], ... } """ log_request(request) try: if ftype not in (None, "usb", "vm"): raise api.InvalidArgument(f"Invalid type '{ftype}'!") from api.File import File return api.response(File().search(file_type=ftype, downloadable_to=sso.role)) except Exception as e: return api.exception_response(e)
def pulseheight(): """Raw PATE pulse height data. GET /api/pulseheight Query parameters: begin - PATE timestamp (Unix timestamp) end - PATE timestamp (Unix timestamp) fields - A comma separated list of fields to return API returns 200 OK and: { ..., "data" : [ { <fields according to query parameter 'fields'> }, ... ], ... } """ log_request(request) try: from api.PulseHeight import PulseHeight return api.response(PulseHeight(request).get()) except Exception as e: return api.exception_response(e)
def api_file_owned(): """Return JSON listing of files owned by currently authenticated person. Slightly 'special' endpoint that accepts only GET method and no parameters of any kind. Data is returned based on the SSO session role. Specially created for Upload and Manage UI, to list user's files.""" log_request(request) try: return api.response(api.File().search(owner=sso.uid or '')) except Exception as e: return api.exception_response(e)
def api_file_schema(): """Create data schema JSON for client FORM creation.""" log_request(request) try: return api.response(api.File().schema()) except Exception as e: return api.exception_response(e)
def pulseheight_aggregate(function): """Aggregated raw PATE pulse height data. GET /api/pulseheight/<string:function> Allowed aggregate functions are: avg, sum, min, max and count. Query parameters: begin - PATE timestamp (Unix timestamp) end - PATE timestamp (Unix timestamp) fields - A comma separated list of fields to return API returns 200 OK and: { ..., "data" : [ { <fields according to query parameter 'fields'> }, ... ], ... } """ log_request(request) try: from api.PulseHeight import PulseHeight if function.lower() not in ('avg', 'sum', 'min', 'max', 'count'): raise api.InvalidArgument( "Function '{}' is not supported!".format(function)) return api.response(PulseHeight(request).get(function)) except Exception as e: return api.exception_response(e)
def register(register_id): """Not yet implemented""" log_request(request) try: raise api.NotImplemented() except Exception as e: return api.exception_response(e)
def api_not_implemented(path=''): """Catch-all route for '/api*' access attempts that do not match any defined routes. "405 Method Not Allowed" JSON reply is returned.""" log_request(request) try: raise api.MethodNotAllowed( "Requested API endpoint ('{}') does not exist!".format("/api/" + path)) except Exception as e: return api.exception_response(e)
def note(): """Search or create note(s). GET /api/note Query parameters: begin - PATE timestamp (Unix timestamp) end - PATE timestamp (Unix timestamp) API responds with 200 OK and: { ..., "data" : [ { "id" : (int), "session_id" : (int), "text" : (str), "created" : (int) }, ... ], ... } POST /api/note No query parameters supported. Required payload: { "text" : (str) } API will respond with 200 OK and: { ..., "data" : { id" : (int) }, ... } """ log_request(request) try: from api.Note import Note note = Note(request) if request.method == 'POST': return api.response(note.create()) elif request.method == 'GET': return api.response(note.search()) else: # Should be impossible raise api.MethodNotAllowed( "'{}' method not allowed for '{}'".format( request.method, request.url_rule)) except Exception as e: return api.exception_response(e)
def api_file_id(id): """Database table row endpoint. Retrieve (GET) or update (PUT) record.""" log_request(request) try: if request.method == 'PUT': return api.response(api.File().update(id, request, sso.uid)) elif request.method == 'GET': return api.response(api.File().fetch(id)) else: raise api.MethodNotAllowed( f"Method {request.method} not supported for this endpoint.") except Exception as e: return api.exception_response(e)
def show_flask_config(): """Middleware (Flask application) configuration. Sensitive entries are censored.""" log_request(request) try: cfg = {} for key in app.config: cfg[key] = app.config[key] # Censor sensitive values for key in cfg: if key in ('SECRET_KEY', 'MYSQL_DATABASE_PASSWORD'): cfg[key] = '<CENSORED>' return api.response((200, cfg)) except Exception as e: return api.exception_response(e)
def show_flask_config(): """Middleware (Flask application) configuration. Sensitive entries are censored.""" # Allow output only when debugging AND when the user is authenticated if not sso.is_authenticated or not app.debug: return api.response((404, {'error': 'Permission Denied'})) log_request(request) try: cfg = {} for key in app.config: cfg[key] = app.config[key] # Censor sensitive values for key in cfg: if key in ('SECRET_KEY', 'MYSQL_DATABASE_PASSWORD'): cfg[key] = '<CENSORED>' return api.response((200, cfg)) except Exception as e: return api.exception_response(e)
def psu_power(): """Agilent power supply remote control. GET /api/psu/power No query parameters supported. Response returns: { ..., "data" : { "power": ["ON", "OFF"], "modified": (int) }, ... } POST /api/psu/power No query parameters supported. Required payload: { "power" : ("ON" | "OFF") } API will respond with 202 Accepted and: { ..., "data" : { "command_id" : (int) }, ... } """ log_request(request) try: include = ['power', 'modified'] if request.method == 'GET': from api.PSU import PSU return api.response(PSU(request).get(include)) else: from api.Command import Command return api.response(Command(request).post("PSU", "SET POWER")) except Exception as e: return api.exception_response(e)
def psu_current_limit(): """Read or Set Current Limit Value from PSU. GET /api/psu/current/limit No query parameters supported. Response returns: { ..., "data" : { "current_limit" : (float), "modified" : (int) }, ... } POST /api/psu/current/limit Required payload: { "current_limit" : (float) } API will respond with 202 Accepted and: { ..., "data" : { "command_id" : (int) }, ... } """ log_request(request) try: include = ['current_limit', 'modified'] if request.method == 'GET': from api.PSU import PSU return api.response(PSU(request).get(include)) else: from api.Command import Command return api.response( Command(request).post("PSU", "SET CURRENT LIMIT")) except Exception as e: return api.exception_response(e)
def psu_voltage(): """Read or set PSU output voltage. GET /api/psu/voltage No query parameters supported. Response returns: { ..., "data" : { "measured_voltage" : (float), "voltage_setting" : (float), "modified" : (int) }, ... } POST /api/psu/voltage Required payload: { "voltage" : (float) } API will respond with 202 Accepted and: { ..., "data" : { "command_id" : (int) }, ... } """ log_request(request) try: include = ['measured_voltage', 'voltage_setting', 'modified'] if request.method == 'GET': from api.PSU import PSU return api.response(PSU(request).get(include)) else: from api.Command import Command return api.response(Command(request).post("PSU", "SET VOLTAGE")) except Exception as e: return api.exception_response(e)
def psu_current(): """Read PSU current. GET /api/psu/voltage No query parameters supported. Response returns: { ..., "data" : { "measured_current" : (float), "current_limit" : (float), "modified" : (int) }, ... }""" log_request(request) try: include = ['measured_current', 'current_limit', 'modified'] from api.PSU import PSU return api.response(PSU(request).get(include)) except Exception as e: return api.exception_response(e)
def api_publish(id=None): """POST with uploaded filename will trigger prepublish procedure. For .OVA files this means reading the included .OVF file for characteristics of the virtual machine. A 'file' table row is inserted and the ID will be returned in the response JSON body { 'id': <int> }.""" try: # Dummy response tuple REMOVE WHEN THIS ROUTE IS COMPLETED! response = (200, { 'data': { '.role': f'{sso.role}', '.is_teacher': f'{str(sso.is_teacher)}', '.is_student': f'{str(sso.is_student)}', '.is_anonymous': f'{str(sso.is_anonymous)}', '.is_authenticated': f'{str(sso.is_authenticated)}' } }) raise api.NotImplemented("Sorry! Not yet implemented!") if not sso.is_teacher: raise api.Unauthorized("Teacher privileges required") if not app.config.get('UPLOAD_FOLDER', None): raise api.InternalError( "Configuration parameter 'UPLOAD_FOLDER' is not set!") if request.method == 'GET': file = request.args.get('file', default=None, type=str) folder = app.config.get('UPLOAD_FOLDER') if not file: raise api.InvalidArgument( "GET Request must provide 'file' URI variable") # Generate JSONForm for editing response = api.Publish(request).preprocess(folder + "/" + file, sso.uid) elif request.method == 'POST': pass except Exception as e: return api.exception_response(e) else: # api.response(code: int, payload: dict) -> Flask.response_class return api.response(response)
def usb_file(): """List of USB disk -type downloadables (column "type" = "usb"). GET /api/usb Query parameters: (none implemented) API returns 200 OK and: { ..., "data" : [ { TBA }, ... ], ... } """ log_request(request) try: from api.File import File return api.response(File(request).search("usb")) except Exception as e: return api.exception_response(e)
def note_by_id(timestamp): """Fetch operator note (identified by timestamp). GET /api/note/<int:timestamp> No query parameters supported. API responds with 200 OK and: { ..., "data" : { "id" : (int), "session_id" : (int), "text" : (str), "created" : (int) }, ... } """ log_request(request) try: from api.Note import Note note = Note(request) api.response(note.fetch(timestamp)) except Exception as e: return api.exception_response(e)
def api_doc(): """JSON API Documentation. Generates API document from the available endpoints. This functionality relies on PEP 257 (https://www.python.org/dev/peps/pep-0257/) convention for docstrings and Flask micro framework route ('rule') mapping to generate basic information listing on all the available REST API functions. This call takes no arguments. GET /sys/api List of API endpoints is returned in JSON. GET /api.html The README.md from /api is prefixed to HTML content. List of API endpoints is included as a table.""" def htmldoc(docstring): """Some HTML formatting for docstrings.""" result = None if docstring: docstring = docstring.replace('<', '<').replace('>', '>') result = "<br/>".join(docstring.split('\n')) + "<br/>" return result try: log_request(request) eplist = [] for rule in app.url_map.iter_rules(): if rule.endpoint != 'static': allowed = [ method for method in rule.methods if method not in ('HEAD', 'OPTIONS') ] methods = ','.join(allowed) eplist.append({ 'service': rule.endpoint, 'methods': methods, 'endpoint': str(rule), 'doc': app.view_functions[rule.endpoint].__doc__ }) # # Sort eplist based on 'endpoint' # eplist = sorted(eplist, key=lambda k: k['endpoint']) if 'api.html' in request.url_rule.rule: try: from ext.markdown2 import markdown with open('api/README.md') as f: readme = markdown(f.read(), extras=["tables"]) except: app.logger.exception("Unable to process 'api/README.md'") readme = '' html = "<!DOCTYPE html><html><head><title>API Listing</title>" html += "<link rel='stylesheet' href='/css/api.css'>" # substitute for favicon html += "<link rel='icon' href='data:;base64,iVBORw0KGgo='>" html += "</head><body>" html += readme html += "<h2>List of Flask routes and Endpoints</h2>" html += "<table class='endpointTable'><tr><th>Service</th><th>Methods</th><th>Endpoint</th><th>Documentation</th></tr>" for row in eplist: html += "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>" \ .format( row['service'], row['methods'], row['endpoint'].replace('<', '<').replace('>', '>'), htmldoc(row['doc']) ) html += "</table></body></html>" # Create Request object response = app.response_class(response=html, status=200, mimetype='text/html') return response else: return api.response((200, {'endpoints': eplist})) except Exception as e: return api.exception_response(e)