generate_schema_config.py 7.29 KB
Newer Older
1 2
from formats import message_header
from formats.mission_plan import mission_plan_schema
3
from formats.mission_plan_encoded import mission_plan_encoded_schema
4
from formats.observation import observation_schema
5
from formats.observation_encoded import observation_encoded_schema
6
from formats.planning_configuration import planning_configuration_schema
7
from formats.platform_status import platform_status_schema
8
from formats.platform_status_encoded import platform_status_encoded_schema
9 10
from formats.survey import survey_schema
from formats.survey_encoded import survey_encoded_schema
11
from formats.acknowledgement import acknowledgement_schema
12
from formats.alert import alert_schema
13 14 15 16

from flasgger import Swagger
from flask import Flask

17 18
import argparse
import json
19 20
import os

21

22 23 24 25 26 27 28
# Enable running on domain sub-path
URL_PREFIX = os.getenv("URL_PREFIX", "")
# Allow env override of default host
FLASK_HOST = os.getenv("FLASK_HOST", "localhost")
# Allow env override of default port 
FLASK_PORT = os.getenv("FLASK_PORT", 5000) 

29 30 31 32 33

swagger_config = {
    "openapi": "3.0.2",
    "swagger_ui": True,
    "specs_route": "/",
34
    "info": {
35 36
        "title": "SoAR Backbone Message Formats",
        "version": "1.0",
37
        "description": "SoAR message protocol in schemas",
38 39 40 41 42 43 44
    },
    "specs": [
        {
            "endpoint": "swagger",
            "route": "/soar_protocol.json",
        }
    ],
45
    "url_prefix": URL_PREFIX,
46 47 48 49 50
    "paths": {},
    "components": {
        "schemas": {
            "MESSAGE": {
                "type": "object",
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
51 52 53
                "description": "Full message definition with"
                + " message-metadata in `header` and different"
                + " message type schemas under `payload`",
54 55 56 57
                "properties": {
                    "header": {
                        "$ref": "#/components/schemas/header",
                    },
58
                    "payload": {"$ref": "#/components/schemas/payload"},
59 60 61 62 63 64
                },
                "required": ["header", "payload"],
            },
            "payload": {
                "discriminator": {
                    "propertyName": "message_type",
65
                    "mapping": {
66
                        "alert": "#/components/schemas/alert",
67
                        "mission_plan": "#/components/schemas/mission_plan",
68 69
                        "mission_plan_encoded": "#/components/schemas/"
                        + "mission_plan_encoded",
70
                        "observation": "#/components/schemas/observation",
71 72
                        "observation_encoded": "#/components/schemas/"
                        + "observation_encoded",
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
73 74
                        "planning_configuration": "#/components/schemas/"
                        + "planning_configuration",
75
                        "platform_status": "#/components/schemas/platform_status",
76 77
                        "platform_status_encoded": "#/components/schemas/"
                        + "platform_status_encoded",
78
                        "acknowledgement": "#/components/schemas/acknowledgement",
79
                        "survey": "#/components/schemas/survey",
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
80
                        "survey_encoded": "#/components/schemas/" + "survey_encoded",
81 82
                    },
                },
83
                "oneOf": [
84
                    {"$ref": "#/components/schemas/alert"},
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
85 86
                    {"$ref": "#/components/schemas/acknowledgement"},
                    {"$ref": "#/components/schemas/mission_plan"},
87
                    {"$ref": "#/components/schemas/mission_plan_encoded"},
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
88
                    {"$ref": "#/components/schemas/observation"},
89
                    {"$ref": "#/components/schemas/observation_encoded"},
Trishna Saeharaseelan's avatar
Trishna Saeharaseelan committed
90 91
                    {"$ref": "#/components/schemas/planning_configuration"},
                    {"$ref": "#/components/schemas/platform_status"},
92
                    {"$ref": "#/components/schemas/platform_status_encoded"},
93 94
                    {"$ref": "#/components/schemas/survey"},
                    {"$ref": "#/components/schemas/survey_encoded"},
95
                ],
96 97 98
            },
            "header": message_header,
            "mission_plan": mission_plan_schema,
99
            "mission_plan_encoded": mission_plan_encoded_schema,
100
            "observation": observation_schema,
101
            "observation_encoded": observation_encoded_schema,
102
            "planning_configuration": planning_configuration_schema,
103
            "platform_status": platform_status_schema,
104
            "platform_status_encoded": platform_status_encoded_schema,
105 106
            "survey": survey_schema,
            "survey_encoded": survey_encoded_schema,
107
            "acknowledgement": acknowledgement_schema,
108
            "alert": alert_schema,
109 110 111 112
        }
    },
}

113 114 115 116
def serve():
    """
    Run as local flask app on port 5000
    """
117 118 119 120 121 122 123 124 125 126
    # Replace schema route to remove invalid 
    # definitions: {}
    # Should be fixed if Flassger 0.9.7 is released
    # 
    # The last release of flasgger was Aug 2020 
    # This bug was fixed in Nov 2021 
    # There is a pre-release from May 2023
    # Until the fix gets released we have to 
    # remove the invalid definitions object 
    # from the spec
127 128 129
    app = Flask(__name__)
    Swagger(app, config=swagger_config, merge=True)
    
130 131 132 133 134 135 136 137 138 139 140
    @app.after_request
    def after_request_decorator(response):
        if type(response).__name__ == 'Response':
            if response.content_type == 'application/json':
                data = response.json
                if 'definitions' in data: 
                    del data['definitions']
                response.data = json.dumps(data)

        return response
    app.run(debug=True, host=FLASK_HOST, port=FLASK_PORT)
141 142


143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
def compile_schema(swagger_config):
    """Extract the output schema from flasgger
        
    The only way I have found to do this is to 
    use a test client to make the GET request 
    for the page

    The function that returns the definition 
    can't be called outside the flask app context
    """
    app = Flask(__name__)
    Swagger(app, config=swagger_config, merge=True)  
    route = swagger_config['specs'][0]['route']
    client = app.test_client()
    response = client.get(route)
    spec = response.json
    return spec


162 163 164 165
def write_schema(swagger_config, file_path):
    """
    Dump schema to specified file
    """
166 167 168
    spec = compile_schema(swagger_config)
    json_schema = json.dumps(spec, indent=2)

169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
    with open(file_path, "w") as f: 
        f.write(json_schema)


def get_options():
    """
    Parse script arguments
    """
    parser = argparse.ArgumentParser(description="Generate the schema",
                                 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument("-s", "--serve", dest="run_flask", action="store_true", help="Run flask app", default=False)
    parser.add_argument("-f", "--file", dest="output_file", action="store_true", help="Save output to schema file", default=False)
    args = parser.parse_args()
    config = vars(args)
    # If no flag is specified default to running the flask server
    if (all(v is False for v in config.values())):
        config["run_flask"] = True
    return config    

188

189 190 191 192 193 194 195
if __name__ == "__main__": 
    # Parse script args 
    config = get_options()    
    
    # Output compiled schema
    if config.get("output_file"):
        write_schema(swagger_config, "project/soar/swagger.json")
196

197 198 199
    # Run flask app
    if config.get("run_flask"):
        serve()