From 6a545e06f39392a0e92261dea7c53de3f5c5d122 Mon Sep 17 00:00:00 2001 From: Thomas Boyer-Chammard <49786685+thomas-bc@users.noreply.github.com> Date: Wed, 11 Oct 2023 14:10:00 -0700 Subject: [PATCH] Update Flask JSON interfaces (#145) * Update Flask JSON interfaces * formatting * Unlock dependency --- setup.py | 2 +- src/fprime_gds/flask/app.py | 28 ++++++++--- src/fprime_gds/flask/json.py | 91 +++++++++++++++++++----------------- 3 files changed, 70 insertions(+), 51 deletions(-) diff --git a/setup.py b/setup.py index c10cb7b4..57d79e4f 100644 --- a/setup.py +++ b/setup.py @@ -108,7 +108,7 @@ python_requires=">=3.7", setup_requires=["setuptools_scm"], install_requires=[ - "flask>=1.1.2,<=2.2.3", + "flask>=2.2.0", "flask_compress>=1.11", "pyzmq>=24.0.1", "pexpect>=4.8.0", diff --git a/src/fprime_gds/flask/app.py b/src/fprime_gds/flask/app.py index 222d5906..f3d28e4a 100644 --- a/src/fprime_gds/flask/app.py +++ b/src/fprime_gds/flask/app.py @@ -65,12 +65,14 @@ def construct_app(): if "FP_FLASK_SETTINGS" in os.environ: app.config.from_envvar("FP_FLASK_SETTINGS") - # JSON encoding setting must come before restful - app.json_encoder = fprime_gds.flask.json.GDSJsonEncoder - app.config["RESTFUL_JSON"] = {"cls": app.json_encoder} + # JSON encoding settings + app.json.default = fprime_gds.flask.json.default + app.config["RESTFUL_JSON"] = {"default": app.json.default} # Standard pipeline creation input_arguments = app.config["STANDARD_PIPELINE_ARGUMENTS"] - args_ns, _ = ParserBase.parse_args([StandardPipelineParser], "n/a", input_arguments, client=True) + args_ns, _ = ParserBase.parse_args( + [StandardPipelineParser], "n/a", input_arguments, client=True + ) pipeline = components.setup_pipelined_components(app.debug, args_ns) # Restful API registration @@ -83,7 +85,11 @@ def construct_app(): api.add_resource( fprime_gds.flask.commands.CommandDictionary, "/dictionary/commands", - resource_class_args=[pipeline.dictionaries.command_name, pipeline.dictionaries.project_version, pipeline.dictionaries.framework_version], + resource_class_args=[ + pipeline.dictionaries.command_name, + pipeline.dictionaries.project_version, + pipeline.dictionaries.framework_version, + ], ) api.add_resource( fprime_gds.flask.commands.CommandHistory, @@ -98,7 +104,11 @@ def construct_app(): api.add_resource( fprime_gds.flask.events.EventDictionary, "/dictionary/events", - resource_class_args=[pipeline.dictionaries.event_id, pipeline.dictionaries.project_version, pipeline.dictionaries.framework_version], + resource_class_args=[ + pipeline.dictionaries.event_id, + pipeline.dictionaries.project_version, + pipeline.dictionaries.framework_version, + ], ) api.add_resource( fprime_gds.flask.events.EventHistory, @@ -108,7 +118,11 @@ def construct_app(): api.add_resource( fprime_gds.flask.channels.ChannelDictionary, "/dictionary/channels", - resource_class_args=[pipeline.dictionaries.channel_id, pipeline.dictionaries.project_version, pipeline.dictionaries.framework_version], + resource_class_args=[ + pipeline.dictionaries.channel_id, + pipeline.dictionaries.project_version, + pipeline.dictionaries.framework_version, + ], ) api.add_resource( fprime_gds.flask.channels.ChannelHistory, diff --git a/src/fprime_gds/flask/json.py b/src/fprime_gds/flask/json.py index 4519da6e..6b68bec9 100644 --- a/src/fprime_gds/flask/json.py +++ b/src/fprime_gds/flask/json.py @@ -20,7 +20,7 @@ def jsonify_base_type(input_type: Type[BaseType]) -> dict: - """ Turn a base type into a JSONable dictionary + """Turn a base type into a JSONable dictionary Convert a BaseType (the type, not an instance) into a jsonable dictionary. BaseTypes are converted by reading the class properties (without __) and creating the object: @@ -36,14 +36,17 @@ class properties json-able dictionary representing the type """ assert issubclass(input_type, BaseType), "Failure to properly encode data" - members = getmembers(input_type, lambda value: not isroutine(value) and not isinstance(value, property)) + members = getmembers( + input_type, + lambda value: not isroutine(value) and not isinstance(value, property), + ) jsonable_dict = {name: value for name, value in members if not name.startswith("_")} jsonable_dict.update({"name": input_type.__name__}) return jsonable_dict def getter_based_json(obj): - """ Converts objects to JSON via get_ methods + """Converts objects to JSON via get_ methods Template functions define a series of get_* methods whose return values need to be serialized. This function handles that data. @@ -80,7 +83,7 @@ def getter_based_json(obj): def minimal_event(obj): - """ Minimal event encoding: time, id, display_text + """Minimal event encoding: time, id, display_text Events need time, id, display_text. No other information from the event is necessary for the display. This will minimally encode the data for JSON. @@ -95,7 +98,7 @@ def minimal_event(obj): def minimal_channel(obj): - """ Minimal channel serialization: time, id, val, and display_text + """Minimal channel serialization: time, id, val, and display_text Minimally serializes channel values for use with the flask layer. This does away with any unnecessary data by serializing only the id, value, and optional display text @@ -106,11 +109,16 @@ def minimal_channel(obj): Returns: JSON compatible python anonymous type (dictionary) """ - return {"time": obj.time, "id": obj.id, "val": obj.val_obj.val, "display_text": obj.display_text} + return { + "time": obj.time, + "id": obj.id, + "val": obj.val_obj.val, + "display_text": obj.display_text, + } def minimal_command(obj): - """ Minimal command serialization: time, id, and args values + """Minimal command serialization: time, id, and args values Minimally serializes the command values for use with the flask layer. This prevents excess data by keeping the data to the minimum instance data for commands including: time, opcode (id), and the value for args. @@ -125,7 +133,7 @@ def minimal_command(obj): def time_type(obj): - """ Time type serialization + """Time type serialization Serializes the time type into a JSON compatible object. @@ -137,49 +145,46 @@ def time_type(obj): """ assert isinstance(obj, TimeType), "Incorrect type for serialization method" return { - "base": obj.timeBase.value, - "context": obj.timeContext, - "seconds": obj.seconds, - "microseconds": obj.useconds - } + "base": obj.timeBase.value, + "context": obj.timeContext, + "seconds": obj.seconds, + "microseconds": obj.useconds, + } def enum_json(obj): - """ Jsonify the python enums! """ + """Jsonify the python enums!""" enum_dict = {"value": str(obj), "values": {}} for enum_val in type(obj): enum_dict["values"][str(enum_val)] = enum_val.value return enum_dict -class GDSJsonEncoder(flask.json.JSONEncoder): - """ - Custom class used to handle GDS object to JSON +JSON_ENCODERS = { + ABCMeta: jsonify_base_type, + UUID: str, + ChData: minimal_channel, + EventData: minimal_event, + CmdData: minimal_command, + TimeType: time_type, +} + + +def default(obj): """ - JSON_ENCODERS = { - ABCMeta: jsonify_base_type, - UUID: str, - ChData: minimal_channel, - EventData: minimal_event, - CmdData: minimal_command, - TimeType: time_type - } + Override the default JSON encoder to pull out a dictionary for our handled types for encoding with the default + encoder built into flask. This function must convert the given object into a JSON compatable python object (e.g. + using lists, dictionaries, strings, and primitive types). - def default(self, obj): - """ - Override the default JSON encoder to pull out a dictionary for our handled types for encoding with the default - encoder built into flask. This function must convert the given object into a JSON compatable python object (e.g. - using lists, dictionaries, strings, and primitive types). - - :param obj: obj to encode - :return: JSON - """ - if type(obj) in self.JSON_ENCODERS: - return self.JSON_ENCODERS[type(obj)](obj) - if isinstance(obj, DataTemplate): - return getter_based_json(obj) - if isinstance(obj, Enum): - return enum_json(obj) - if isinstance(obj, ValueType): - return obj.val - return flask.json.JSONEncoder.default(self, obj) + :param obj: obj to encode + :return: JSON + """ + if type(obj) in JSON_ENCODERS: + return JSON_ENCODERS[type(obj)](obj) + if isinstance(obj, DataTemplate): + return getter_based_json(obj) + if isinstance(obj, Enum): + return enum_json(obj) + if isinstance(obj, ValueType): + return obj.val + return flask.json.provider.DefaultJSONProvider.default(obj)