Skip to content

Commit d6cad91

Browse files
committed
add htmldump apis
1 parent 39f998b commit d6cad91

File tree

5 files changed

+107
-6
lines changed

5 files changed

+107
-6
lines changed

servicestack/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,16 @@
6868
'is_optional',
6969
'is_list',
7070
'is_dict',
71+
'is_builtin',
7172
'generic_args',
7273
'generic_arg',
7374
'inspect_vars',
7475
'dump',
7576
'printdump',
76-
'table',
77+
'table',
7778
'printtable',
79+
'htmldump',
80+
'printhtmldump',
7881
'lowercase',
7982
'uppercase',
8083
'snakecase',
@@ -120,8 +123,8 @@
120123
qsvalue, resolve_httpmethod
121124

122125
from .reflection import TypeConverters, to_json, from_json, all_keys, to_dict, convert, is_optional, \
123-
is_list, is_dict, generic_args, generic_arg, inspect_vars, dump, printdump, table, \
124-
printtable
126+
is_list, is_dict, is_builtin, generic_args, generic_arg, inspect_vars, dump, printdump, table, \
127+
printtable, htmldump, printhtmldump
125128

126129
from .utils import \
127130
lowercase, uppercase, snakecase, camelcase, capitalcase, pascalcase, titlecase, clean_camelcase, \

servicestack/clients.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ def send_url(self, path: str, method: str = None, response_as: Type = None, body
310310

311311
return self.send_request(info)
312312

313-
def send(self, request, method="POST", body: Any = None, args: Dict[str, Any] = None):
313+
def send(self, request, method: Any = None, body: Any = None, args: Dict[str, Any] = None):
314314
if not isinstance(request, IReturn) and not isinstance(request, IReturnVoid):
315315
raise TypeError(f"'{nameof(request)}' does not implement IReturn or IReturnVoid")
316316

servicestack/reflection.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import base64
2+
import collections
23
import decimal
34
import inspect
45
import json
@@ -91,9 +92,16 @@ def identity(x: Any): return x
9192
_BUILT_IN_TYPES = {str, bool, int, float, decimal.Decimal, datetime, timedelta, bytes, bytearray, complex}
9293

9394

95+
def is_builtin(t: Type):
96+
try:
97+
return t in _BUILT_IN_TYPES or issubclass(t, Enum)
98+
except Exception as e: # throws if t is not hashable
99+
return False
100+
101+
94102
def to_dict(obj: Any, key_case: Callable[[str], str] = identity, remove_empty: bool = True):
95103
t = type(obj)
96-
if obj is None or t in _BUILT_IN_TYPES or issubclass(t, Enum):
104+
if obj is None or is_builtin(t):
97105
return obj
98106
if is_list(t):
99107
to = []
@@ -157,7 +165,7 @@ def _json_encoder(obj: Any):
157165
return float(obj)
158166
if t in _JSON_TYPES:
159167
return obj
160-
if t in _BUILT_IN_TYPES or (type(obj) == type and issubclass(obj, Enum)):
168+
if is_builtin(t):
161169
return _str(obj)
162170
raise TypeError(f"Unsupported Type in JSON encoding: {t}")
163171

@@ -363,7 +371,11 @@ def from_json(into: Type, json_str: str):
363371
# inspect utils
364372
def all_keys(obj):
365373
keys = []
374+
if not isinstance(obj, collections.Iterable):
375+
return keys
366376
for o in obj:
377+
if is_builtin(type(o)):
378+
continue
367379
for key in o:
368380
key = _str(key)
369381
if key is not None and key not in keys:
@@ -477,3 +489,54 @@ def table(objs, headers=None):
477489

478490
def printtable(obj, headers=None):
479491
print(table(obj, headers))
492+
493+
def htmllist(d: dict):
494+
sb: List[str] = ["<table><tbody>"]
495+
for k, v in d.items():
496+
sb.append(f"<tr><th>{_str(k)}</th><td>{_str(v)}</td></tr>")
497+
sb.append("</tbody></table>")
498+
return ''.join(sb)
499+
500+
def htmldump(objs, headers=None):
501+
map_rows = to_dict(objs)
502+
t = type(map_rows)
503+
if is_builtin(t):
504+
return _str(map_rows)
505+
if is_dict(t):
506+
return htmllist(map_rows)
507+
508+
if headers is None:
509+
headers = all_keys(map_rows)
510+
511+
# print(headers, map_rows)
512+
513+
sb: List[str] = ["<table>"]
514+
row = ["<thead><tr>"]
515+
for k in headers:
516+
row.append(f"<th>{k}</th>")
517+
row.append("</tr></head>")
518+
if len(row) > 2:
519+
sb.append(''.join(row))
520+
sb.append("<tbody>")
521+
522+
rows = []
523+
for item in map_rows:
524+
rows.append("<tr>")
525+
if len(headers) > 0:
526+
row = []
527+
for k in headers:
528+
val = item[k] if k in item else ""
529+
row.append(f"<td>{val}</td>")
530+
rows.append(''.join(row))
531+
else:
532+
rows.append(f"<td>{item}</td>")
533+
rows.append("</tr>")
534+
535+
sb.append(''.join(rows))
536+
sb.append("</tbody>")
537+
sb.append("</table>")
538+
return '\n'.join(sb)
539+
540+
541+
def printhtmldump(obj, headers=None):
542+
print(htmldump(obj, headers))

tests/test_client_utils.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,27 @@
22
"""
33

44
import unittest
5+
import re
56
from .dtos import *
67
from servicestack import to_json, qsvalue, resolve_httpmethod
78

89

10+
@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE)
11+
@dataclass
12+
class GetLocationsResponse:
13+
locations: Optional[List[str]] = None
14+
15+
16+
# @Route("/locations")
17+
@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE)
18+
@dataclass
19+
class GetLocations(IReturn[GetLocationsResponse], IGet):
20+
pass
21+
22+
23+
def sanitize_html(s: str): return re.sub(r"\s", "", s)
24+
25+
926
class ClientUtils(unittest.TestCase):
1027

1128
def test_json_encode(self):
@@ -27,3 +44,20 @@ def test_does_resolve_IVerbs_from_request_DTO_interface_marker(self):
2744
self.assertEqual(resolve_httpmethod(SendGet()), "GET")
2845
self.assertEqual(resolve_httpmethod(SendPost()), "POST")
2946
self.assertEqual(resolve_httpmethod(SendPut()), "PUT")
47+
self.assertEqual(resolve_httpmethod(GetLocations()), "GET")
48+
49+
def test_can_htmldump_different_types(self):
50+
self.assertEqual(htmldump(1), "1")
51+
self.assertEqual(htmldump("A"), "A")
52+
self.assertEqual(htmldump({"A": 1, "B": 2}),
53+
"<table><tbody><tr><th>A</th><td>1</td></tr><tr><th>B</th><td>2</td></tr></tbody></table>")
54+
self.assertEqual(sanitize_html(htmldump(["A", 1])),
55+
"<table><tbody><tr><td>A</td></tr><tr><td>1</td></tr></tbody></table>")
56+
self.assertEqual(sanitize_html(htmldump([{"A": 1, "B": 2}, {"A": 3, "B": 4}])),
57+
"<table><thead><tr><th>A</th><th>B</th></tr></head><tbody><tr><td>1</td><td>2</td></tr><tr><td>3</td><td>4</td></tr></tbody></table>")
58+
59+
def test_can_send_GetLocations(self):
60+
client = JsonServiceClient("https://covid-vac-watch.netcore.io")
61+
response = client.send(GetLocations())
62+
printdump(response)
63+
printhtmldump(response.locations)

tests/test_inspect.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ def test_does_support_FindTechnologies(self):
4949
printtable(response.results)
5050
printtable(response.results, headers=['id', 'name', 'vendor_name', 'view_count', 'fav_count'])
5151
printtable(to_dict(response.results, key_case=titlecase))
52+
printhtmldump(response.results)
5253
inspect_vars({"response": response})

0 commit comments

Comments
 (0)