1
+ import base64
2
+ import datetime
1
3
import decimal
2
4
import inspect
3
- import typing
4
- from dataclasses import field , fields , asdict , is_dataclass
5
+ import json
6
+ from dataclasses_json import dataclass_json
7
+ from dataclasses import dataclass , field , fields , asdict , is_dataclass
8
+ from datetime import datetime , timedelta
5
9
from enum import Enum , IntEnum
6
10
from typing import Callable , get_args , Type , get_origin , ForwardRef , Union
11
+ from typing import TypeVar , Optional , Dict , List , Any
7
12
from urllib .parse import urljoin , quote_plus
8
13
14
+ import marshmallow .fields as mf
9
15
import requests
10
16
from requests .exceptions import HTTPError
11
17
from requests .models import Response
12
18
from stringcase import camelcase , snakecase , uppercase
13
19
14
- from servicestack .dtos import *
15
- from servicestack . fields import *
20
+ from servicestack .dtos import IReturn , IReturnVoid , IGet , IPost , IPut , IPatch , \
21
+ IDelete , ResponseStatus , EmptyResponse , GetAccessToken , GetAccessTokenResponse
16
22
from servicestack .log import Log
23
+ from servicestack .utils import is_list , is_dict , to_timespan , to_datetime , \
24
+ to_bytearray , from_bytearray , from_datetime , from_timespan , is_optional , \
25
+ generic_args , generic_arg
17
26
18
27
JSON_MIME_TYPE = "application/json"
19
28
AUTHORIZATION_HEADER = "Authorization"
@@ -120,8 +129,8 @@ def qsvalue(arg):
120
129
return "{" + ',' .join ([k + ":" + qsvalue (v ) for k , v in arg ]) + "}"
121
130
if arg_type is str :
122
131
return quote_plus (arg )
123
- if arg_type is bytes or arg_type is bytearray :
124
- return base64 . b64encode (arg ). decode ( "utf-8" )
132
+ if arg_type in TypeConverters . serializers :
133
+ return TypeConverters . serialize (arg )
125
134
return quote_plus (str (arg ))
126
135
127
136
@@ -132,7 +141,9 @@ def append_querystring(url: str, args: dict[str, Any]):
132
141
if val is None :
133
142
continue
134
143
url += '&' if '?' in url else '?'
135
- url += key + '=' + qsvalue (val )
144
+ qs_val = qsvalue (val )
145
+ if qs_val is not None :
146
+ url += key + '=' + qs_val
136
147
return url
137
148
138
149
@@ -144,6 +155,19 @@ def _empty(x):
144
155
return x is None or x == {} or x == []
145
156
146
157
158
+ def _asdict (obj : Any ):
159
+ if obj is None :
160
+ return {}
161
+ if is_dataclass (obj ):
162
+ d = asdict (obj )
163
+ to = {}
164
+ for k , v in d .items ():
165
+ to [camelcase (k )] = v
166
+ else :
167
+ to = obj .__dict__
168
+ return clean_any (to )
169
+
170
+
147
171
def _clean_list (d : list ):
148
172
return [v for v in (clean_any (v ) for v in d ) if not _empty (v )]
149
173
@@ -164,7 +188,7 @@ def clean_any(d):
164
188
165
189
def _json_encoder (obj : Any ):
166
190
if is_dataclass (obj ):
167
- return clean_any ( asdict ( obj ) )
191
+ return _asdict ( obj )
168
192
if hasattr (obj , '__dict__' ):
169
193
return vars (obj )
170
194
if isinstance (obj , datetime ):
@@ -187,16 +211,50 @@ def to_json(obj: Any):
187
211
188
212
189
213
class TypeConverters :
214
+ serializers : dict [Type , Callable [[Any ], Any ]]
190
215
deserializers : dict [Type , Callable [[Any ], Any ]]
191
216
192
217
@staticmethod
193
- def register_deserializer (cls : Type , deserializer : Callable [[Any ], Any ]):
194
- TypeConverters .deserializers [cls ] = deserializer
218
+ def register (cls : Type , serializer : Callable [[Any ], Any ] = None , deserializer : Callable [[Any ], Any ] = None ):
219
+ if serializer is not None :
220
+ TypeConverters .serializers [cls ] = serializer
221
+ if deserializer is not None :
222
+ TypeConverters .deserializers [cls ] = deserializer
195
223
224
+ @staticmethod
225
+ def serialize (obj : Any ):
226
+ cls = type (obj )
227
+ if cls in TypeConverters .serializers :
228
+ serializer = TypeConverters .serializers [cls ]
229
+ try :
230
+ return serializer (obj )
231
+ except Exception as e :
232
+ Log .error (f"serializer(obj) { cls } ({ obj } )" , e )
233
+ raise e
234
+
235
+ @staticmethod
236
+ def deserialize (cls : Type , obj : Any ):
237
+ deserializer = TypeConverters .deserializers [cls ]
238
+ try :
239
+ return deserializer (obj )
240
+ except Exception as e :
241
+ Log .error (f"deserializer(obj) { cls } ({ obj } )" , e )
242
+ raise e
196
243
244
+
245
+ TypeConverters .serializers = {
246
+ datetime : to_datetime ,
247
+ timedelta : to_timespan ,
248
+ bytes : to_bytearray ,
249
+ bytearray : to_bytearray ,
250
+ }
197
251
TypeConverters .deserializers = {
198
252
mf .DateTime : from_datetime ,
199
253
mf .TimeDelta : from_timespan ,
254
+ datetime : from_datetime ,
255
+ timedelta : from_timespan ,
256
+ bytes : from_bytearray ,
257
+ bytearray : from_bytearray ,
200
258
}
201
259
202
260
@@ -239,7 +297,7 @@ def sanitize_name(s: str):
239
297
return s .replace ('_' , '' ).upper ()
240
298
241
299
242
- def enum_get (cls : Enum , key : Union [str , int ]):
300
+ def enum_get (cls : Union [ Enum , Type ] , key : Union [str , int ]):
243
301
if type (key ) == int or issubclass (cls , IntEnum ):
244
302
return cls (key )
245
303
try :
@@ -490,7 +548,7 @@ def refresh_token_cookie(self):
490
548
def create_url_from_dto (self , method : str , request : Any ):
491
549
url = urljoin (self .reply_base_url , nameof (request ))
492
550
if not has_request_body (method ):
493
- url = append_querystring (url , request . __dict__ )
551
+ url = append_querystring (url , _asdict ( request ) )
494
552
return url
495
553
496
554
def get (self , request : IReturn [T ], args : Dict [str , Any ] = None ) -> T :
@@ -723,7 +781,7 @@ def create_request(self, info: SendContext):
723
781
body_not_request_dto = info .request and info .body
724
782
if body_not_request_dto :
725
783
url = urljoin (self .reply_base_url , nameof (info .request ))
726
- url = append_querystring (url , clean_any ( asdict ( info .request ) ))
784
+ url = append_querystring (url , _asdict ( info .request ))
727
785
else :
728
786
url = self .create_url_from_dto (info .method , body )
729
787
0 commit comments