Skip to content

Commit 39f998b

Browse files
committed
update table fn's
1 parent aee6631 commit 39f998b

File tree

4 files changed

+86
-61
lines changed

4 files changed

+86
-61
lines changed

servicestack/clients.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
from servicestack.log import Log
1717
from servicestack.reflection import TypeConverters, to_dict, nameof, is_list, is_dict, _resolve_forwardref, \
1818
has_type_vars, _dict_with_string_keys, _get_type_vars_map, from_json, to_json
19-
from servicestack.utils import ex_message
19+
from servicestack.utils import ex_message, clean_camelcase
20+
2021

2122
JSON_MIME_TYPE = "application/json"
2223
AUTHORIZATION_HEADER = "Authorization"
@@ -142,7 +143,6 @@ def exec(self):
142143
Log.debug(
143144
f"{using}.request({self.method}): url={self.url}, headers={self.headers}, data={self.body_string}")
144145

145-
response: Optional[Response] = None
146146
if has_request_body(self.method):
147147
if self.session is not None:
148148
response = self.session.request(self.method, self.url, data=self.body_string, headers=self.headers)
@@ -241,7 +241,7 @@ def refresh_token_cookie(self):
241241
def create_url_from_dto(self, method: str, request: Any):
242242
url = urljoin(self.reply_base_url, nameof(request))
243243
if not has_request_body(method):
244-
url = append_querystring(url, to_dict(request))
244+
url = append_querystring(url, to_dict(request, key_case=clean_camelcase))
245245
return url
246246

247247
def get(self, request: IReturn[T], args: Dict[str, Any] = None) -> T:
@@ -329,14 +329,15 @@ def send(self, request, method="POST", body: Any = None, args: Dict[str, Any] =
329329
args=args,
330330
response_as=response_as))
331331

332-
def assert_valid_batch_request(self, requests: list):
333-
if not isinstance(requests, list):
334-
raise TypeError(f"'{nameof(requests)}' is not a List")
332+
@staticmethod
333+
def assert_valid_batch_request(request_dtos: list):
334+
if not isinstance(request_dtos, list):
335+
raise TypeError(f"'{nameof(request_dtos)}' is not a List")
335336

336-
if len(requests) == 0:
337+
if len(request_dtos) == 0:
337338
return []
338339

339-
request = requests[0]
340+
request = request_dtos[0]
340341
if not isinstance(request, IReturn) and not isinstance(request, IReturnVoid):
341342
raise TypeError(f"'{nameof(request)}' does not implement IReturn or IReturnVoid")
342343

@@ -345,31 +346,31 @@ def assert_valid_batch_request(self, requests: list):
345346
raise TypeError(f"Could not resolve Response Type for '{nameof(request)}'")
346347
return request, item_response_as
347348

348-
def send_all(self, requests: List[IReturn[T]]):
349-
request, item_response_as = self.assert_valid_batch_request(requests)
349+
def send_all(self, request_dtos: List[IReturn[T]]):
350+
request, item_response_as = self.assert_valid_batch_request(request_dtos)
350351
url = urljoin(self.reply_base_url, nameof(request) + "[]")
351352

352353
return self.send_request(SendContext(
353354
session=self._session,
354355
headers=self.headers.copy(),
355356
method="POST",
356357
url=url,
357-
request=list(requests),
358+
request=list(request_dtos),
358359
body=None,
359360
body_string=None,
360361
args=None,
361362
response_as=list.__class_getitem__(item_response_as)))
362363

363-
def send_all_oneway(self, requests: list):
364-
request, item_response_as = self.assert_valid_batch_request(requests)
364+
def send_all_oneway(self, request_dtos: list):
365+
request, item_response_as = self.assert_valid_batch_request(request_dtos)
365366
url = urljoin(self.oneway_base_url, nameof(request) + "[]")
366367

367368
self.send_request(SendContext(
368369
session=self._session,
369370
headers=self.headers.copy(),
370371
method="POST",
371372
url=url,
372-
request=list(requests),
373+
request=list(request_dtos),
373374
body=None,
374375
body_string=None,
375376
args=None,
@@ -474,7 +475,7 @@ def create_request(self, info: SendContext):
474475
body_not_request_dto = info.request and info.body
475476
if body_not_request_dto:
476477
url = urljoin(self.reply_base_url, nameof(info.request))
477-
url = append_querystring(url, to_dict(info.request))
478+
url = append_querystring(url, to_dict(info.request, key_case=clean_camelcase))
478479
else:
479480
url = self.create_url_from_dto(info.method, body)
480481

servicestack/reflection.py

Lines changed: 62 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import base64
2-
import dataclasses
32
import decimal
43
import inspect
54
import json
5+
import math
66
import numbers
77
import os
88
import pathlib
99
import platform
1010
import typing
11-
from dataclasses import asdict
1211
from dataclasses import fields, is_dataclass
1312
from datetime import datetime, timedelta
14-
from enum import Enum, IntEnum
13+
from enum import Enum, IntEnum, EnumMeta
1514
from functools import reduce
15+
from types import MappingProxyType
1616
from typing import Callable, Type, get_origin, ForwardRef, Union, TypeVar, get_args
1717
from typing import List, Dict, Any
1818

@@ -29,7 +29,7 @@ def is_optional(cls: Type): return get_origin(cls) is Union and type(None) in ge
2929
def is_list(cls: Type): return cls == typing.List or cls == list or get_origin(cls) == list
3030

3131

32-
def is_dict(cls: Type): return cls == typing.Dict or cls == dict or get_origin(cls) == dict
32+
def is_dict(cls: Type): return cls == typing.Dict or cls == dict or get_origin(cls) == dict or cls == MappingProxyType
3333

3434

3535
def nameof(instance):
@@ -78,31 +78,51 @@ def _empty(x):
7878
return x is None or x == {} or x == []
7979

8080

81-
def to_dict(obj: Any):
82-
if obj is None:
83-
return {}
81+
def _str(x: Any):
82+
if type(x) == str:
83+
return x
84+
return f"{x}"
85+
86+
87+
def identity(x: Any): return x
88+
89+
90+
_JSON_TYPES = {str, bool, int, float}
91+
_BUILT_IN_TYPES = {str, bool, int, float, decimal.Decimal, datetime, timedelta, bytes, bytearray, complex}
92+
93+
94+
def to_dict(obj: Any, key_case: Callable[[str], str] = identity, remove_empty: bool = True):
8495
t = type(obj)
96+
if obj is None or t in _BUILT_IN_TYPES or issubclass(t, Enum):
97+
return obj
8598
if is_list(t):
8699
to = []
87100
for o in obj:
88-
to.append(to_dict(o))
89-
return to
101+
use_val = to_dict(o, key_case=key_case, remove_empty=remove_empty)
102+
if not remove_empty or use_val is not None:
103+
to.append(use_val)
90104
elif is_dict(t):
91105
to = {}
92-
for k in obj:
93-
to[k] = to_dict(obj[k])
94-
return to
95-
if is_dataclass(obj):
96-
d = asdict(obj)
106+
for k, v in obj.items():
107+
use_key = key_case(_str(k))
108+
use_val = to_dict(v, key_case=key_case, remove_empty=remove_empty)
109+
if not remove_empty or use_val is not None:
110+
to[use_key] = use_val
111+
elif hasattr(obj, 'to_dict'): # dataclass
112+
d = obj.to_dict()
97113
to = {}
98114
for k, v in d.items():
99-
use_key = camelcase(k)
100-
if use_key[-1] == '_':
101-
use_key = use_key[0:-1]
102-
to[use_key] = v
115+
use_key = key_case(_str(k))
116+
use_val = to_dict(v, key_case=key_case, remove_empty=remove_empty)
117+
if not remove_empty or use_val is not None:
118+
to[use_key] = use_val
103119
elif hasattr(obj, '__dict__'):
104-
return obj.__dict__
105-
return clean_any(to)
120+
to = to_dict(vars(obj), key_case=key_case, remove_empty=remove_empty)
121+
else:
122+
to = obj
123+
if remove_empty:
124+
return clean_any(to)
125+
return to
106126

107127

108128
def _clean_list(d: list):
@@ -115,19 +135,18 @@ def _clean_dict(d: dict):
115135

116136
def clean_any(d):
117137
"""recursively remove empty lists, empty dicts, or None elements from a dictionary"""
118-
if not isinstance(d, (dict, list)):
119-
return d
120-
elif isinstance(d, list):
138+
if is_dict(d):
139+
return _clean_dict(d)
140+
elif is_list(d):
121141
return _clean_list(d)
122142
else:
123-
return _clean_dict(d)
143+
return d
124144

125145

126146
def _json_encoder(obj: Any):
127-
if is_dataclass(obj):
147+
t = type(obj)
148+
if is_dataclass(t) or is_dict(t):
128149
return to_dict(obj)
129-
if hasattr(obj, '__dict__'):
130-
return vars(obj)
131150
if isinstance(obj, datetime):
132151
return obj.isoformat()
133152
if isinstance(obj, timedelta):
@@ -136,14 +155,15 @@ def _json_encoder(obj: Any):
136155
return base64.b64encode(obj).decode('ascii')
137156
if isinstance(obj, decimal.Decimal):
138157
return float(obj)
139-
raise TypeError(f"Unsupported Type in JSON encoding: {type(obj)}")
158+
if t in _JSON_TYPES:
159+
return obj
160+
if t in _BUILT_IN_TYPES or (type(obj) == type and issubclass(obj, Enum)):
161+
return _str(obj)
162+
raise TypeError(f"Unsupported Type in JSON encoding: {t}")
140163

141164

142165
def to_json(obj: Any, indent=None):
143-
if is_dataclass(obj):
144-
obj_dict = clean_any(obj.to_dict())
145-
return json.dumps(obj_dict, indent=indent, default=_json_encoder)
146-
return json.dumps(obj, indent=indent, default=_json_encoder)
166+
return json.dumps(to_dict(obj), indent=indent, default=_json_encoder)
147167

148168

149169
class TypeConverters:
@@ -316,7 +336,7 @@ def convert(into: Type, obj: Any, substitute_types: Dict[Type, type] = None):
316336
except Exception as e:
317337
Log.error(f"into().deserialize(obj) {into}({obj})", e)
318338
raise e
319-
elif is_type and issubclass(into, Enum):
339+
elif is_type and (issubclass(into, Enum) or into == EnumMeta):
320340
try:
321341
return enum_get(into, obj)
322342
except Exception as e:
@@ -345,7 +365,8 @@ def all_keys(obj):
345365
keys = []
346366
for o in obj:
347367
for key in o:
348-
if not key in keys:
368+
key = _str(key)
369+
if key is not None and key not in keys:
349370
keys.append(key)
350371
return keys
351372

@@ -392,7 +413,7 @@ def _align_center(s: str, length: int, pad: str = ' '):
392413
if length < 0:
393414
return ""
394415
nlen = len(s)
395-
half = (length // 2 - nlen // 2)
416+
half = math.floor(length / 2 - nlen / 2)
396417
odds = abs((nlen % 2) - (length % 2))
397418
return (pad * (half + 1)) + s + (pad * (half + 1 + odds))
398419

@@ -407,28 +428,28 @@ def _align_right(s: str, length: int, pad: str = ' '):
407428

408429

409430
def _align_auto(obj: Any, length: int, pad: str = ' '):
410-
s = f"{obj}"
431+
s = _str(obj)
411432
if len(s) <= length:
412433
if isinstance(obj, numbers.Number):
413434
return _align_right(s, length, pad)
414435
return _align_left(s, length, pad)
415436
return s
416437

417438

418-
def dumptable(objs, headers=None):
439+
def table(objs, headers=None):
419440
if not is_list(type(objs)):
420441
raise TypeError('objs must be a list')
421442
map_rows = to_dict(objs)
422443
if headers is None:
423-
headers = _allkeys(map_rows)
444+
headers = all_keys(map_rows)
424445
col_sizes: Dict[str, int] = {}
425446

426447
for k in headers:
427448
max = len(k)
428449
for row in map_rows:
429450
if k in row:
430451
col = row[k]
431-
val_size = len(f"{col}")
452+
val_size = len(_str(col))
432453
if val_size > max:
433454
max = val_size
434455
col_sizes[k] = max
@@ -454,5 +475,5 @@ def dumptable(objs, headers=None):
454475
return '\n'.join(sb)
455476

456477

457-
def printdumptable(obj, headers=None):
458-
print(dumptable(obj, headers))
478+
def printtable(obj, headers=None):
479+
print(table(obj, headers))

tests/test_inspect.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def test_does_dump(self):
3333
printdump(org_repos[0:3])
3434

3535
print(f'\nTop 10 {org_name} Repos:')
36-
printdumptable(org_repos[0:10], headers=['name', 'lang', 'watchers', 'forks'])
36+
printtable(org_repos[0:10], headers=['name', 'lang', 'watchers', 'forks'])
3737

3838
inspect_vars({'org_repos': org_repos})
3939

@@ -42,8 +42,11 @@ def test_does_support_FindTechnologies(self):
4242
response = client.send(FindTechnologies(
4343
ids=[1, 2, 3],
4444
vendor_name="Google",
45-
take=5))
45+
take=5,
46+
fields='id,name,vendorName,createdBy,viewCount,favCount'))
4647

47-
printdump(response)
48-
printdumptable(response.results, headers=['id', 'name', 'vendorName', 'createdBy', 'viewCount', 'favCount'])
48+
# printdump(response)
49+
printtable(response.results)
50+
printtable(response.results, headers=['id', 'name', 'vendor_name', 'view_count', 'fav_count'])
51+
printtable(to_dict(response.results, key_case=titlecase))
4952
inspect_vars({"response": response})

tests/test_jsonserviceclient.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ def test_does_serialize_dates_correctly_via_get_request(self):
3232
def test_should_generate_default_value(self):
3333
client = create_test_client()
3434
request = HelloTypes(bool_=False, int_=0)
35-
request_url = append_querystring(TEST_URL, to_dict(request))
35+
request_url = append_querystring(TEST_URL, to_dict(request, key_case=clean_camelcase))
3636
self.assertEqual(request_url, TEST_URL + "?bool=false&int=0")

0 commit comments

Comments
 (0)