Skip to content

Commit abe7652

Browse files
authored
🔧 Fix CI installation failure for json2html caused by setuptools 78.0.1 update (#7415)
1 parent 0a16428 commit abe7652

File tree

6 files changed

+247
-11
lines changed

6 files changed

+247
-11
lines changed

services/web/server/requirements/_base.in

-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ faker # Only used in dev-mode for proof-of-concepts
3737
gunicorn[setproctitle]
3838
httpx
3939
jinja_app_loader # email
40-
json2html
4140
jsondiff
4241
msgpack
4342
openpyxl # excel

services/web/server/requirements/_base.txt

-2
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,6 @@ jinja2==3.1.2
308308
# -c requirements/../../../../requirements/constraints.txt
309309
# aiohttp-jinja2
310310
# swagger-ui-py
311-
json2html==1.3.0
312-
# via -r requirements/_base.in
313311
jsondiff==2.0.0
314312
# via -r requirements/_base.in
315313
jsonschema==3.2.0

services/web/server/src/simcore_service_webserver/publications/_handlers.py renamed to services/web/server/src/simcore_service_webserver/publications/_rest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from aiohttp import MultipartReader, hdrs, web
44
from common_library.json_serialization import json_dumps
5-
from json2html import json2html # type: ignore[import-untyped]
65
from servicelib.aiohttp import status
76
from servicelib.mimetype_constants import (
87
MIMETYPE_APPLICATION_JSON,
@@ -15,6 +14,7 @@
1514
from ..login.storage import AsyncpgStorage, get_plugin_storage
1615
from ..login.utils_email import AttachmentTuple, send_email_from_template, themed
1716
from ..products import products_web
17+
from ._utils import json2html
1818

1919
_logger = logging.getLogger(__name__)
2020

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
"""
2+
This module provides functionality to convert JSON data into an HTML table format.
3+
It is a snapshot of the `json2html` library to avoid compatibility issues with
4+
specific versions of `setuptools`.
5+
6+
Classes:
7+
- Json2Html: A class that provides methods to convert JSON data into HTML tables
8+
or lists, with options for customization.
9+
10+
Functions:
11+
----------
12+
- Json2Html.convert: Converts JSON data into an HTML table or list format.
13+
- Json2Html.column_headers_from_list_of_dicts: Determines column headers for a list of dictionaries.
14+
- Json2Html.convert_json_node: Dispatches JSON input based on its type and processes it into HTML.
15+
- Json2Html.convert_list: Converts a JSON list into an HTML table or list.
16+
- Json2Html.convert_object: Converts a JSON object into an HTML table.
17+
18+
Attributes:
19+
-----------
20+
- json2html: An instance of the Json2Html class for direct use.
21+
22+
Notes:
23+
------
24+
- This module supports Python 2.7+ and Python 3.x.
25+
- It uses `OrderedDict` to preserve the order of keys in JSON objects.
26+
- The `html_escape` function is used to escape HTML characters in text.
27+
28+
License:
29+
MIT License
30+
31+
Source:
32+
-------
33+
Snapshot of https://github.com/softvar/json2html/blob/0a223c7b3e5dce286811fb12bbab681e7212ebfe/json2html/jsonconv.py
34+
JSON 2 HTML Converter
35+
=====================
36+
37+
(c) Varun Malhotra 2013
38+
Source Code: https://github.com/softvar/json2html
39+
40+
41+
Contributors:
42+
-------------
43+
1. Michel Müller (@muellermichel), https://github.com/softvar/json2html/pull/2
44+
2. Daniel Lekic (@lekic), https://github.com/softvar/json2html/pull/17
45+
46+
LICENSE: MIT
47+
--------
48+
"""
49+
50+
# pylint: skip-file
51+
#
52+
# NOTE: Snapshot of https://github.com/softvar/json2html/blob/0a223c7b3e5dce286811fb12bbab681e7212ebfe/json2html/jsonconv.py
53+
# to avoid failure to install this module with `setuptools 78.0.1` due to
54+
# deprecated feature that this library still uses
55+
#
56+
57+
58+
import sys
59+
60+
if sys.version_info[:2] < (2, 7):
61+
import simplejson as json_parser
62+
from ordereddict import OrderedDict
63+
else:
64+
import json as json_parser
65+
from collections import OrderedDict
66+
67+
if sys.version_info[:2] < (3, 0):
68+
from cgi import escape as html_escape
69+
70+
text = unicode
71+
text_types = (unicode, str)
72+
else:
73+
from html import escape as html_escape
74+
75+
text = str
76+
text_types = (str,)
77+
78+
79+
class Json2Html:
80+
def convert(
81+
self,
82+
json="",
83+
table_attributes='border="1"',
84+
clubbing=True,
85+
encode=False,
86+
escape=True,
87+
):
88+
"""
89+
Convert JSON to HTML Table format
90+
"""
91+
# table attributes such as class, id, data-attr-*, etc.
92+
# eg: table_attributes = 'class = "table table-bordered sortable"'
93+
self.table_init_markup = "<table %s>" % table_attributes
94+
self.clubbing = clubbing
95+
self.escape = escape
96+
json_input = None
97+
if not json:
98+
json_input = {}
99+
elif type(json) in text_types:
100+
try:
101+
json_input = json_parser.loads(json, object_pairs_hook=OrderedDict)
102+
except ValueError as e:
103+
# so the string passed here is actually not a json string
104+
# - let's analyze whether we want to pass on the error or use the string as-is as a text node
105+
if "Expecting property name" in text(e):
106+
# if this specific json loads error is raised, then the user probably actually wanted to pass json, but made a mistake
107+
raise e
108+
json_input = json
109+
else:
110+
json_input = json
111+
converted = self.convert_json_node(json_input)
112+
if encode:
113+
return converted.encode("ascii", "xmlcharrefreplace")
114+
return converted
115+
116+
def column_headers_from_list_of_dicts(self, json_input):
117+
"""
118+
This method is required to implement clubbing.
119+
It tries to come up with column headers for your input
120+
"""
121+
if (
122+
not json_input
123+
or not hasattr(json_input, "__getitem__")
124+
or not hasattr(json_input[0], "keys")
125+
):
126+
return None
127+
column_headers = json_input[0].keys()
128+
for entry in json_input:
129+
if (
130+
not hasattr(entry, "keys")
131+
or not hasattr(entry, "__iter__")
132+
or len(entry.keys()) != len(column_headers)
133+
):
134+
return None
135+
for header in column_headers:
136+
if header not in entry:
137+
return None
138+
return column_headers
139+
140+
def convert_json_node(self, json_input):
141+
"""
142+
Dispatch JSON input according to the outermost type and process it
143+
to generate the super awesome HTML format.
144+
We try to adhere to duck typing such that users can just pass all kinds
145+
of funky objects to json2html that *behave* like dicts and lists and other
146+
basic JSON types.
147+
"""
148+
if type(json_input) in text_types:
149+
if self.escape:
150+
return html_escape(text(json_input))
151+
else:
152+
return text(json_input)
153+
if hasattr(json_input, "items"):
154+
return self.convert_object(json_input)
155+
if hasattr(json_input, "__iter__") and hasattr(json_input, "__getitem__"):
156+
return self.convert_list(json_input)
157+
return text(json_input)
158+
159+
def convert_list(self, list_input):
160+
"""
161+
Iterate over the JSON list and process it
162+
to generate either an HTML table or a HTML list, depending on what's inside.
163+
If suppose some key has array of objects and all the keys are same,
164+
instead of creating a new row for each such entry,
165+
club such values, thus it makes more sense and more readable table.
166+
167+
@example:
168+
jsonObject = {
169+
"sampleData": [
170+
{"a":1, "b":2, "c":3},
171+
{"a":5, "b":6, "c":7}
172+
]
173+
}
174+
OUTPUT:
175+
_____________________________
176+
| | | | |
177+
| | a | c | b |
178+
| sampleData |---|---|---|
179+
| | 1 | 3 | 2 |
180+
| | 5 | 7 | 6 |
181+
-----------------------------
182+
183+
@contributed by: @muellermichel
184+
"""
185+
if not list_input:
186+
return ""
187+
converted_output = ""
188+
column_headers = None
189+
if self.clubbing:
190+
column_headers = self.column_headers_from_list_of_dicts(list_input)
191+
if column_headers is not None:
192+
converted_output += self.table_init_markup
193+
converted_output += "<thead>"
194+
converted_output += (
195+
"<tr><th>" + "</th><th>".join(column_headers) + "</th></tr>"
196+
)
197+
converted_output += "</thead>"
198+
converted_output += "<tbody>"
199+
for list_entry in list_input:
200+
converted_output += "<tr><td>"
201+
converted_output += "</td><td>".join(
202+
[
203+
self.convert_json_node(list_entry[column_header])
204+
for column_header in column_headers
205+
]
206+
)
207+
converted_output += "</td></tr>"
208+
converted_output += "</tbody>"
209+
converted_output += "</table>"
210+
return converted_output
211+
212+
# so you don't want or need clubbing eh? This makes @muellermichel very sad... ;(
213+
# alright, let's fall back to a basic list here...
214+
converted_output = "<ul><li>"
215+
converted_output += "</li><li>".join(
216+
[self.convert_json_node(child) for child in list_input]
217+
)
218+
converted_output += "</li></ul>"
219+
return converted_output
220+
221+
def convert_object(self, json_input):
222+
"""
223+
Iterate over the JSON object and process it
224+
to generate the super awesome HTML Table format
225+
"""
226+
if not json_input:
227+
return "" # avoid empty tables
228+
converted_output = self.table_init_markup + "<tr>"
229+
converted_output += "</tr><tr>".join(
230+
[
231+
"<th>%s</th><td>%s</td>"
232+
% (self.convert_json_node(k), self.convert_json_node(v))
233+
for k, v in json_input.items()
234+
]
235+
)
236+
converted_output += "</tr></table>"
237+
return converted_output
238+
239+
240+
json2html = Json2Html()
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
""" publications management subsystem
1+
"""publications management subsystem"""
22

3-
"""
43
import logging
54

65
from aiohttp import web
@@ -9,22 +8,22 @@
98

109
from ..email.plugin import setup_email
1110
from ..products.plugin import setup_products
12-
from . import _handlers
11+
from . import _rest
1312

14-
logger = logging.getLogger(__name__)
13+
_logger = logging.getLogger(__name__)
1514

1615

1716
@app_module_setup(
1817
__name__,
1918
ModuleCategory.ADDON,
2019
depends=["simcore_service_webserver.rest"],
2120
settings_name="WEBSERVER_PUBLICATIONS",
22-
logger=logger,
21+
logger=_logger,
2322
)
2423
def setup_publications(app: web.Application):
2524
assert app[APP_SETTINGS_KEY].WEBSERVER_PUBLICATIONS # nosec
2625

2726
setup_email(app)
2827
setup_products(app)
2928

30-
app.router.add_routes(_handlers.routes)
29+
app.router.add_routes(_rest.routes)

services/web/server/tests/unit/with_dbs/03/login/test_login_utils_emails.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from aiohttp import web
1212
from aiohttp.test_utils import make_mocked_request
1313
from faker import Faker
14-
from json2html import json2html
1514
from pytest_mock import MockerFixture
1615
from pytest_simcore.helpers.typing_env import EnvVarsDict
1716
from simcore_service_webserver.application_settings import setup_settings
@@ -23,6 +22,7 @@
2322
get_template_path,
2423
send_email_from_template,
2524
)
25+
from simcore_service_webserver.publications._utils import json2html
2626
from simcore_service_webserver.statics._constants import FRONTEND_APPS_AVAILABLE
2727

2828

0 commit comments

Comments
 (0)