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 += (
+ "| " + " | ".join(column_headers) + " |
"
+ )
+ converted_output += ""
+ converted_output += ""
+ for list_entry in list_input:
+ converted_output += "| "
+ converted_output += " | ".join(
+ [
+ self.convert_json_node(list_entry[column_header])
+ for column_header in column_headers
+ ]
+ )
+ converted_output += " |
"
+ converted_output += ""
+ 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 = "- "
+ converted_output += "
- ".join(
+ [self.convert_json_node(child) for child in list_input]
+ )
+ 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