generate_schema_config.py 7.44 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
# 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")
Dan Jones's avatar
Dan Jones committed
26 27
# Allow env override of default port
FLASK_PORT = os.getenv("FLASK_PORT", 5000)
28

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
        }
    },
}

Dan Jones's avatar
Dan Jones committed
113 114

def configure_flask(swagger_config):
115 116
    app = Flask(__name__)
    Swagger(app, config=swagger_config, merge=True)
Dan Jones's avatar
Dan Jones committed
117

118 119
    @app.after_request
    def after_request_decorator(response):
Dan Jones's avatar
Dan Jones committed
120 121
        if type(response).__name__ == "Response":
            if response.content_type == "application/json":
122
                data = response.json
Dan Jones's avatar
Dan Jones committed
123 124
                if "definitions" in data:
                    del data["definitions"]
125 126 127
                response.data = json.dumps(data)

        return response
Dan Jones's avatar
Dan Jones committed
128

Dan Jones's avatar
Dan Jones committed
129 130 131 132 133 134 135
    return app


def serve(swagger_config):
    """
    Run as local flask app on port 5000
    """
Dan Jones's avatar
Dan Jones committed
136
    # Replace schema route to remove invalid
Dan Jones's avatar
Dan Jones committed
137 138
    # definitions: {}
    # Should be fixed if Flassger 0.9.7 is released
Dan Jones's avatar
Dan Jones committed
139 140 141
    #
    # The last release of flasgger was Aug 2020
    # This bug was fixed in Nov 2021
Dan Jones's avatar
Dan Jones committed
142
    # There is a pre-release from May 2023
Dan Jones's avatar
Dan Jones committed
143 144
    # Until the fix gets released we have to
    # remove the invalid definitions object
Dan Jones's avatar
Dan Jones committed
145
    # from the spec
Dan Jones's avatar
Dan Jones committed
146
    app = configure_flask(swagger_config)
147
    app.run(debug=True, host=FLASK_HOST, port=FLASK_PORT)
148 149


150 151
def compile_schema(swagger_config):
    """Extract the output schema from flasgger
Dan Jones's avatar
Dan Jones committed
152 153 154

    The only way I have found to do this is to
    use a test client to make the GET request
155 156
    for the page

Dan Jones's avatar
Dan Jones committed
157
    The function that returns the definition
158 159
    can't be called outside the flask app context
    """
Dan Jones's avatar
Dan Jones committed
160 161
    app = configure_flask(swagger_config)
    route = swagger_config["specs"][0]["route"]
162 163 164 165 166 167
    client = app.test_client()
    response = client.get(route)
    spec = response.json
    return spec


168 169 170 171
def write_schema(swagger_config, file_path):
    """
    Dump schema to specified file
    """
172 173 174
    spec = compile_schema(swagger_config)
    json_schema = json.dumps(spec, indent=2)

Dan Jones's avatar
Dan Jones committed
175
    with open(file_path, "w") as f:
176 177 178 179 180 181 182
        f.write(json_schema)


def get_options():
    """
    Parse script arguments
    """
Dan Jones's avatar
Dan Jones committed
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
    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,
    )
203 204 205
    args = parser.parse_args()
    config = vars(args)
    # If no flag is specified default to running the flask server
Dan Jones's avatar
Dan Jones committed
206
    if all(v is False for v in config.values()):
207
        config["run_flask"] = True
Dan Jones's avatar
Dan Jones committed
208 209
    return config

210

Dan Jones's avatar
Dan Jones committed
211 212 213
if __name__ == "__main__":
    # Parse script args
    config = get_options()
214

215 216 217
    # Output compiled schema
    if config.get("output_file"):
        write_schema(swagger_config, "project/soar/swagger.json")
218

219 220
    # Run flask app
    if config.get("run_flask"):
Dan Jones's avatar
Dan Jones committed
221
        serve(swagger_config)