diff --git a/services/web/server/requirements/_base.in b/services/web/server/requirements/_base.in index f8e72f62ca61..aecaa5793bb0 100644 --- a/services/web/server/requirements/_base.in +++ b/services/web/server/requirements/_base.in @@ -37,7 +37,6 @@ faker # Only used in dev-mode for proof-of-concepts gunicorn[setproctitle] httpx jinja_app_loader # email -json2html jsondiff msgpack openpyxl # excel diff --git a/services/web/server/requirements/_base.txt b/services/web/server/requirements/_base.txt index cdc22f6b9e67..7cf4d029ed90 100644 --- a/services/web/server/requirements/_base.txt +++ b/services/web/server/requirements/_base.txt @@ -308,8 +308,6 @@ jinja2==3.1.2 # -c requirements/../../../../requirements/constraints.txt # aiohttp-jinja2 # swagger-ui-py -json2html==1.3.0 - # via -r requirements/_base.in jsondiff==2.0.0 # via -r requirements/_base.in jsonschema==3.2.0 diff --git a/services/web/server/src/simcore_service_webserver/publications/_handlers.py b/services/web/server/src/simcore_service_webserver/publications/_rest.py similarity index 98% rename from services/web/server/src/simcore_service_webserver/publications/_handlers.py rename to services/web/server/src/simcore_service_webserver/publications/_rest.py index b1ccad24ed29..35ccbac61a5f 100644 --- a/services/web/server/src/simcore_service_webserver/publications/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/publications/_rest.py @@ -2,7 +2,6 @@ from aiohttp import MultipartReader, hdrs, web from common_library.json_serialization import json_dumps -from json2html import json2html # type: ignore[import-untyped] from servicelib.aiohttp import status from servicelib.mimetype_constants import ( MIMETYPE_APPLICATION_JSON, @@ -15,6 +14,7 @@ from ..login.storage import AsyncpgStorage, get_plugin_storage from ..login.utils_email import AttachmentTuple, send_email_from_template, themed from ..products import products_web +from ._utils import json2html _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/publications/_utils.py b/services/web/server/src/simcore_service_webserver/publications/_utils.py new file mode 100644 index 000000000000..0e1ab899db16 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/publications/_utils.py @@ -0,0 +1,240 @@ +""" +This module provides functionality to convert JSON data into an HTML table format. +It is a snapshot of the `json2html` library to avoid compatibility issues with +specific versions of `setuptools`. + +Classes: +- Json2Html: A class that provides methods to convert JSON data into HTML tables + or lists, with options for customization. + +Functions: +---------- +- Json2Html.convert: Converts JSON data into an HTML table or list format. +- Json2Html.column_headers_from_list_of_dicts: Determines column headers for a list of dictionaries. +- Json2Html.convert_json_node: Dispatches JSON input based on its type and processes it into HTML. +- Json2Html.convert_list: Converts a JSON list into an HTML table or list. +- Json2Html.convert_object: Converts a JSON object into an HTML table. + +Attributes: +----------- +- json2html: An instance of the Json2Html class for direct use. + +Notes: +------ +- This module supports Python 2.7+ and Python 3.x. +- It uses `OrderedDict` to preserve the order of keys in JSON objects. +- The `html_escape` function is used to escape HTML characters in text. + +License: +MIT License + +Source: +------- +Snapshot of https://github.com/softvar/json2html/blob/0a223c7b3e5dce286811fb12bbab681e7212ebfe/json2html/jsonconv.py +JSON 2 HTML Converter +===================== + +(c) Varun Malhotra 2013 +Source Code: https://github.com/softvar/json2html + + +Contributors: +------------- +1. Michel Müller (@muellermichel), https://github.com/softvar/json2html/pull/2 +2. Daniel Lekic (@lekic), https://github.com/softvar/json2html/pull/17 + +LICENSE: MIT +-------- +""" + +# pylint: skip-file +# +# NOTE: Snapshot of https://github.com/softvar/json2html/blob/0a223c7b3e5dce286811fb12bbab681e7212ebfe/json2html/jsonconv.py +# to avoid failure to install this module with `setuptools 78.0.1` due to +# deprecated feature that this library still uses +# + + +import sys + +if sys.version_info[:2] < (2, 7): + import simplejson as json_parser + from ordereddict import OrderedDict +else: + import json as json_parser + from collections import OrderedDict + +if sys.version_info[:2] < (3, 0): + from cgi import escape as html_escape + + text = unicode + text_types = (unicode, str) +else: + from html import escape as html_escape + + text = str + text_types = (str,) + + +class Json2Html: + def convert( + self, + json="", + table_attributes='border="1"', + clubbing=True, + encode=False, + escape=True, + ): + """ + Convert JSON to HTML Table format + """ + # table attributes such as class, id, data-attr-*, etc. + # eg: table_attributes = 'class = "table table-bordered sortable"' + self.table_init_markup = "" % table_attributes + self.clubbing = clubbing + self.escape = escape + json_input = None + if not json: + json_input = {} + elif type(json) in text_types: + try: + json_input = json_parser.loads(json, object_pairs_hook=OrderedDict) + except ValueError as e: + # so the string passed here is actually not a json string + # - let's analyze whether we want to pass on the error or use the string as-is as a text node + if "Expecting property name" in text(e): + # if this specific json loads error is raised, then the user probably actually wanted to pass json, but made a mistake + raise e + json_input = json + else: + json_input = json + converted = self.convert_json_node(json_input) + if encode: + return converted.encode("ascii", "xmlcharrefreplace") + return converted + + def column_headers_from_list_of_dicts(self, json_input): + """ + This method is required to implement clubbing. + It tries to come up with column headers for your input + """ + if ( + not json_input + or not hasattr(json_input, "__getitem__") + or not hasattr(json_input[0], "keys") + ): + return None + column_headers = json_input[0].keys() + for entry in json_input: + if ( + not hasattr(entry, "keys") + or not hasattr(entry, "__iter__") + or len(entry.keys()) != len(column_headers) + ): + return None + for header in column_headers: + if header not in entry: + return None + return column_headers + + def convert_json_node(self, json_input): + """ + Dispatch JSON input according to the outermost type and process it + to generate the super awesome HTML format. + We try to adhere to duck typing such that users can just pass all kinds + of funky objects to json2html that *behave* like dicts and lists and other + basic JSON types. + """ + if type(json_input) in text_types: + if self.escape: + return html_escape(text(json_input)) + else: + return text(json_input) + if hasattr(json_input, "items"): + return self.convert_object(json_input) + if hasattr(json_input, "__iter__") and hasattr(json_input, "__getitem__"): + return self.convert_list(json_input) + return text(json_input) + + def convert_list(self, list_input): + """ + Iterate over the JSON list and process it + to generate either an HTML table or a HTML list, depending on what's inside. + If suppose some key has array of objects and all the keys are same, + instead of creating a new row for each such entry, + club such values, thus it makes more sense and more readable table. + + @example: + jsonObject = { + "sampleData": [ + {"a":1, "b":2, "c":3}, + {"a":5, "b":6, "c":7} + ] + } + OUTPUT: + _____________________________ + | | | | | + | | a | c | b | + | sampleData |---|---|---| + | | 1 | 3 | 2 | + | | 5 | 7 | 6 | + ----------------------------- + + @contributed by: @muellermichel + """ + if not list_input: + return "" + converted_output = "" + column_headers = None + if self.clubbing: + column_headers = self.column_headers_from_list_of_dicts(list_input) + if column_headers is not None: + converted_output += self.table_init_markup + converted_output += "" + converted_output += ( + "" + ) + converted_output += "" + converted_output += "" + for list_entry in list_input: + converted_output += "" + converted_output += "" + converted_output += "
" + "".join(column_headers) + "
" + converted_output += "".join( + [ + self.convert_json_node(list_entry[column_header]) + for column_header in column_headers + ] + ) + converted_output += "
" + return converted_output + + # so you don't want or need clubbing eh? This makes @muellermichel very sad... ;( + # alright, let's fall back to a basic list here... + converted_output = "" + return converted_output + + def convert_object(self, json_input): + """ + Iterate over the JSON object and process it + to generate the super awesome HTML Table format + """ + if not json_input: + return "" # avoid empty tables + converted_output = self.table_init_markup + "" + converted_output += "".join( + [ + "%s%s" + % (self.convert_json_node(k), self.convert_json_node(v)) + for k, v in json_input.items() + ] + ) + converted_output += "" + return converted_output + + +json2html = Json2Html() diff --git a/services/web/server/src/simcore_service_webserver/publications/plugin.py b/services/web/server/src/simcore_service_webserver/publications/plugin.py index e1460d653dc7..a85b83cf3b84 100644 --- a/services/web/server/src/simcore_service_webserver/publications/plugin.py +++ b/services/web/server/src/simcore_service_webserver/publications/plugin.py @@ -1,6 +1,5 @@ -""" publications management subsystem +"""publications management subsystem""" -""" import logging from aiohttp import web @@ -9,9 +8,9 @@ from ..email.plugin import setup_email from ..products.plugin import setup_products -from . import _handlers +from . import _rest -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) @app_module_setup( @@ -19,7 +18,7 @@ ModuleCategory.ADDON, depends=["simcore_service_webserver.rest"], settings_name="WEBSERVER_PUBLICATIONS", - logger=logger, + logger=_logger, ) def setup_publications(app: web.Application): assert app[APP_SETTINGS_KEY].WEBSERVER_PUBLICATIONS # nosec @@ -27,4 +26,4 @@ def setup_publications(app: web.Application): setup_email(app) setup_products(app) - app.router.add_routes(_handlers.routes) + app.router.add_routes(_rest.routes) diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_utils_emails.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_utils_emails.py index 24a13a85d454..1eb7f810faa0 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_utils_emails.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_utils_emails.py @@ -11,7 +11,6 @@ from aiohttp import web from aiohttp.test_utils import make_mocked_request from faker import Faker -from json2html import json2html from pytest_mock import MockerFixture from pytest_simcore.helpers.typing_env import EnvVarsDict from simcore_service_webserver.application_settings import setup_settings @@ -23,6 +22,7 @@ get_template_path, send_email_from_template, ) +from simcore_service_webserver.publications._utils import json2html from simcore_service_webserver.statics._constants import FRONTEND_APPS_AVAILABLE