Skip to content

Commit 58e9f61

Browse files
committed
Working AllTypes with tests
1 parent 5d5beaa commit 58e9f61

File tree

6 files changed

+835
-592
lines changed

6 files changed

+835
-592
lines changed

servicestack/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,11 @@
5555
'StringResponse',
5656
'StringsResponse',
5757
'AuditBase',
58-
'qsvalue'
58+
'qsvalue',
59+
'resolve_httpmethod',
60+
'Bytes'
5961
]
6062

61-
from .servicestack import JsonServiceClient, json_encode, qsvalue, resolve_httpmethod
63+
from .servicestack import JsonServiceClient, json_encode, qsvalue, resolve_httpmethod, Bytes
64+
from .utils import *
6265
from .client_dtos import *

servicestack/log.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,37 +15,35 @@ def log(self, level:LogLevel, msg:str, e:Exception=None):
1515
class ConsoleLogger(Logger):
1616
def log(self, level:LogLevel, msg:str, e:Exception=None):
1717
print(f"{level}: {msg}")
18+
if e:
19+
print(e)
1820

1921
class NullLogger(Logger):
2022
def log(self, level:LogLevel, msg:str, e:Exception=None):
2123
pass
2224

2325
class Log:
24-
levels: list[LogLevel] = [LogLevel.DEBUG,LogLevel.WARN,LogLevel.ERROR]
26+
levels: list[LogLevel] = [LogLevel.WARN,LogLevel.ERROR]
2527
logger:Logger = ConsoleLogger()
2628

27-
@property
28-
def debug_enabled(self):
29+
def debug_enabled():
2930
return LogLevel.DEBUG in Log.levels
30-
@property
31-
def info_enabled(self):
31+
def info_enabled():
3232
return LogLevel.INFO in Log.levels
33-
@property
34-
def warn_enabled(self):
33+
def warn_enabled():
3534
return LogLevel.WARN in Log.levels
36-
@property
37-
def error_enabled(self):
35+
def error_enabled():
3836
return LogLevel.ERROR in Log.levels
3937

4038
def debug(msg:str):
41-
if (Log.debug_enabled):
39+
if Log.debug_enabled():
4240
Log.logger.log(LogLevel.DEBUG, msg)
4341
def info(msg:str):
44-
if (Log.info_enabled):
42+
if Log.info_enabled():
4543
Log.logger.log(LogLevel.INFO, msg)
4644
def warn(msg:str, e:Exception=None):
47-
if (Log.warn_enabled):
45+
if Log.warn_enabled():
4846
Log.logger.log(LogLevel.WARN, msg, e)
4947
def error(msg:str, e:Exception=None):
50-
if (Log.error_enabled):
48+
if Log.error_enabled():
5149
Log.logger.log(LogLevel.ERROR, msg, e)

servicestack/servicestack.py

Lines changed: 137 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,53 @@
11
from datetime import datetime, date, timedelta
22
import json
3+
from re import L
4+
35
from servicestack.utils import to_timespan
46

57
from requests.models import HTTPError, Response
68

7-
from servicestack.client_dtos import IDelete, IGet, IPatch, IPost, IPut, IReturn, IReturnVoid, ResponseStatus
9+
from servicestack.client_dtos import IDelete, IGet, IPatch, IPost, IPut, IReturn, IReturnVoid, ResponseStatus, KeyValuePair
810
from servicestack.log import Log
11+
from servicestack.utils import *
912

1013
from typing import Callable, TypeVar, Generic, Optional, Dict, List, Tuple, get_args, Any, Type
11-
from dataclasses import dataclass, field, asdict, is_dataclass
12-
from dataclasses_json import dataclass_json, LetterCase, Undefined
14+
from dataclasses import dataclass, field, fields, asdict, is_dataclass, Field
15+
from dataclasses_json import dataclass_json, LetterCase, Undefined, config, mm
1316
from urllib.parse import urljoin, urlencode, quote_plus
17+
from stringcase import camelcase, snakecase
18+
import marshmallow.fields as mf
1419
import requests
1520
import base64
21+
import decimal
1622

1723
JSON_MIME_TYPE = "application/json"
1824

25+
class Bytes(mf.Field):
26+
def _serialize(self, value, attr, obj, **kwargs):
27+
return to_bytearray(value)
28+
29+
def _deserialize(self, value, attr, data, **kwargs):
30+
return from_bytearray(value)
31+
32+
mm.TYPES[timedelta] = mf.DateTime
33+
mm.TYPES[KeyValuePair] = KeyValuePair[str,str]
34+
mm.TYPES[bytes] = Bytes
35+
mm.TYPES[Bytes] = Bytes
36+
37+
@dataclass
38+
class A:
39+
l: list
40+
li: list[int]
41+
gl: List[int]
42+
ol: Optional[List[int]]
43+
od: Optional[Dict[int,str]]
44+
int_:int = field(metadata=config(field_name="@name"), default=0)
45+
int_string_map: Optional[Dict[int,str]] = None
46+
1947
def dump(obj):
2048
print("")
2149
for attr in dir(obj):
22-
print("obj.%s = %r" % (attr, getattr(obj, attr)))
50+
print(f"obj.{attr} = {getattr(obj, attr)}")
2351
print("")
2452

2553
def _resolve_response_type(request):
@@ -103,9 +131,97 @@ def _json_encoder(obj:Any):
103131

104132
def json_encode(obj:Any):
105133
if is_dataclass(obj):
106-
return json.dumps(clean_any(asdict(obj)), default=_json_encoder)
134+
# return obj.to_json()
135+
return json.dumps(clean_any(obj.to_dict()), default=_json_encoder)
107136
return json.dumps(obj, default=_json_encoder)
108137

138+
def _json_decoder(obj:Any):
139+
# print('ZZZZZZZZZZZZZZZZZ')
140+
# print(type(obj))
141+
return obj
142+
143+
class TypeConverters:
144+
converters: dict[Type, Callable[[Any],Any]]
145+
146+
def register(type:Type, converter:Callable[[Any],Any]):
147+
TypeConverters.converters[type] = converter
148+
149+
TypeConverters.converters = {
150+
mf.Integer: int,
151+
mf.Float: float,
152+
mf.Decimal: decimal.Decimal,
153+
mf.String: str,
154+
mf.Boolean: bool,
155+
mf.DateTime: from_datetime,
156+
mf.TimeDelta: from_timespan,
157+
Bytes: from_bytearray,
158+
bytes: from_bytearray,
159+
}
160+
161+
def is_optional(cls:Type): return f"{cls}".startswith("typing.Optional")
162+
def is_list(cls:Type): return f"{cls}".startswith("typing.List")
163+
def is_dict(cls:Type): return f"{cls}".startswith("typing.Dict")
164+
165+
def generic_arg(cls:Type): return cls.__args__[0]
166+
def generic_args(cls:Type): return cls.__args__
167+
def unwrap(cls:Type):
168+
if is_optional(cls):
169+
return generic_arg(cls)
170+
return cls
171+
172+
def dict_get(name:str, obj:dict, case:Callable[[str],str] = None):
173+
if name in obj: return obj[name]
174+
if case:
175+
nameCase = case(name)
176+
if nameCase in obj: return obj[nameCase]
177+
nameSnake = snakecase(name)
178+
if nameSnake in obj: return obj[nameSnake]
179+
nameCamel = camelcase(name)
180+
if nameCamel in obj: return obj[nameCamel]
181+
if name.endswith('_'):
182+
return dict_get(name.rstrip('_'), obj, case)
183+
return None
184+
185+
def convert(into:Type, obj:Any):
186+
if obj is None: return None
187+
into = unwrap(into)
188+
if is_dataclass(into):
189+
to = {}
190+
for f in fields(into):
191+
val = dict_get(f.name, obj)
192+
to[f.name] = convert(f.type, val)
193+
# print(f"to[{f.name}] = {to[f.name]}")
194+
return into(**to)
195+
elif is_list(into):
196+
el_type = generic_arg(into)
197+
to = []
198+
for item in obj:
199+
to.append(convert(el_type, item))
200+
return to
201+
elif is_dict(into):
202+
key_type, val_type = generic_args(into)
203+
to = {}
204+
for key, val in obj.items():
205+
to_key = convert(key_type, key)
206+
to_val = convert(val_type, val)
207+
to[to_key] = to_val
208+
return to
209+
else:
210+
if into in TypeConverters.converters:
211+
converter = TypeConverters.converters[into]
212+
try:
213+
return converter(obj)
214+
except Exception as e:
215+
Log.error(f"ERROR converter(obj) {into}({obj})", e)
216+
raise e
217+
else:
218+
# print(f"TRY {obj} into {into}")
219+
try:
220+
return into(obj)
221+
except Exception as e:
222+
Log.error(f"ERROR into(obj) {into}({obj})", e)
223+
raise e
224+
109225
def ex_message(e:Exception):
110226
if hasattr(e,'message'):
111227
return e.message
@@ -260,14 +376,23 @@ def _create_response(self, response:Response, info:SendContext):
260376
return response.content
261377

262378
json_str = response.text
379+
if Log.debug_enabled: Log.debug(f"json_str: {json_str}")
263380

264381
if not into:
265382
return json.loads(json_str)
266383

267384
if into is str:
268385
return json_str
269386

270-
res_dto = into.schema().loads(json_str)
387+
try:
388+
# res_dto = into.schema().loads(json_str, object_hook=_json_decoder)
389+
390+
json_obj = json.loads(json_str)
391+
res_dto = convert(into, json_obj)
392+
except Exception as e:
393+
Log.error(f"Failed to deserialize into {into}: {e}", e)
394+
raise e
395+
271396
return res_dto
272397

273398
def _raise_error(self, e:Exception):
@@ -284,7 +409,7 @@ def _handle_error(self, hold_res:Response, e:Exception):
284409

285410
if e is HTTPError:
286411
web_ex.status_code = e.response.status_code
287-
print(dump(e))
412+
if Log.debug_enabled: Log.debug(f"{e}")
288413

289414
if hold_res:
290415
pass
@@ -309,7 +434,7 @@ def send_request(self, info:SendContext):
309434
if info.args:
310435
url = append_querystring(url, info.args)
311436
except Exception as e:
312-
if (Log.debug_enabled): Log.debug(f"send_request(): {ex_message(e)}")
437+
if Log.debug_enabled(): Log.debug(f"send_request(): {ex_message(e)}")
313438
return self._handle_error(None, e)
314439

315440
info.url = url
@@ -331,9 +456,12 @@ def send_request(self, info:SendContext):
331456
try:
332457
response = self._resend_request(info)
333458
res_dto = self._create_response(response,info)
459+
460+
if Log.debug_enabled(): Log.debug(f"res_dto = {type(res_dto)}")
461+
334462
return res_dto
335463
except Exception as e:
336-
if (Log.debug_enabled): Log.debug(f"send_request() create_response: {ex_message(e)}")
464+
if Log.debug_enabled(): Log.debug(f"send_request() create_response: {ex_message(e)}")
337465
return self._handle_error(response, e)
338466

339467

0 commit comments

Comments
 (0)