1
1
from datetime import datetime , date , timedelta
2
2
import json
3
3
from re import L
4
+ from requests .api import request
4
5
5
6
from requests .models import Response
6
7
from requests .exceptions import HTTPError
10
11
from servicestack .utils import *
11
12
from servicestack .fields import *
12
13
13
- from typing import Callable , TypeVar , Generic , Optional , Dict , List , Tuple , get_args , Any , Type
14
+ from typing import Callable , TypeVar , Generic , Optional , Dict , List , Tuple , get_args , Any , Type , get_origin , get_type_hints , ForwardRef
14
15
from dataclasses import dataclass , field , fields , asdict , is_dataclass , Field
15
16
from dataclasses_json import dataclass_json , LetterCase , Undefined , config , mm
16
17
from urllib .parse import urljoin , urlencode , quote_plus
23
24
24
25
JSON_MIME_TYPE = "application/json"
25
26
26
- @dataclass
27
- class A :
28
- l : list
29
- li : list [int ]
30
- gl : List [int ]
31
- ol : Optional [List [int ]]
32
- od : Optional [Dict [int ,str ]]
33
- int_ :int = field (metadata = config (field_name = "@name" ), default = 0 )
34
- int_string_map : Optional [Dict [int ,str ]] = None
35
-
36
- def dump (obj ):
27
+ def _dump (obj ):
37
28
print ("" )
38
29
for attr in dir (obj ):
39
30
print (f"obj.{ attr } = { getattr (obj , attr )} " )
@@ -43,7 +34,10 @@ def _resolve_response_type(request):
43
34
if isinstance (request , IReturn ):
44
35
for cls in request .__orig_bases__ :
45
36
if hasattr (cls ,'__args__' ):
46
- return cls .__args__ [0 ]
37
+ candidate = cls .__args__ [0 ]
38
+ if type (candidate ) == ForwardRef :
39
+ return _resolve_forwardref (candidate , type (request ))
40
+ return candidate
47
41
if isinstance (request , IReturnVoid ):
48
42
return type (None )
49
43
return None
@@ -116,6 +110,8 @@ def _json_encoder(obj:Any):
116
110
return to_timespan (obj )
117
111
if isinstance (obj , bytes ):
118
112
return base64 .b64encode (obj ).decode ('ascii' )
113
+ if isinstance (obj , decimal .Decimal ):
114
+ return float (obj )
119
115
raise TypeError (f"Unsupported Type in JSON encoding: { type (obj )} " )
120
116
121
117
def to_json (obj :Any ):
@@ -135,12 +131,28 @@ def register(type:Type, converter:Callable[[Any],Any]):
135
131
}
136
132
137
133
def is_optional (cls :Type ): return f"{ cls } " .startswith ("typing.Optional" )
138
- def is_list (cls :Type ): return f"{ cls } " .startswith ("typing.List" )
139
- def is_dict (cls :Type ): return f"{ cls } " .startswith ("typing.Dict" )
134
+ def is_list (cls :Type ):
135
+ return cls == typing .List or cls == list or get_origin (cls ) == list
136
+ def is_dict (cls :Type ):
137
+ return cls == typing .Dict or cls == dict or get_origin (cls ) == dict
138
+
139
+ def generic_arg (cls :Type ): return generic_args (cls )[0 ]
140
+ def generic_args (cls :Type ):
141
+ if not hasattr (cls , '__args__' ):
142
+ raise TypeError (f"{ cls } is not a Generic Type" )
143
+ return cls .__args__
144
+
145
+ def _resolve_forwardref (cls :Type , orig :Type = None ):
146
+ type_name = cls .__forward_arg__
147
+ if not orig is None and orig .__name__ == type_name :
148
+ return orig
149
+ if not type_name in globals ():
150
+ raise TypeError (f"Could not resolve ForwardRef('{ type_name } ')" )
151
+ return globals ()[type_name ]
140
152
141
- def generic_arg (cls :Type ): return cls .__args__ [0 ]
142
- def generic_args (cls :Type ): return cls .__args__
143
153
def unwrap (cls :Type ):
154
+ if type (cls ) == ForwardRef :
155
+ cls = _resolve_forwardref (cls )
144
156
if is_optional (cls ):
145
157
return generic_arg (cls )
146
158
return cls
@@ -158,28 +170,48 @@ def dict_get(name:str, obj:dict, case:Callable[[str],str] = None):
158
170
return dict_get (name .rstrip ('_' ), obj , case )
159
171
return None
160
172
161
- def convert (into :Type , obj :Any ):
173
+ def _resolve_type (cls :Type , substitute_types :Dict [str ,type ]):
174
+ if substitute_types is None : return cls
175
+ return substitute_types [cls ] if cls in substitute_types else cls
176
+
177
+ def convert (into :Type , obj :Any , substitute_types :Dict [str ,type ]= None ):
162
178
if obj is None : return None
163
179
into = unwrap (into )
180
+ into = _resolve_type (into , substitute_types )
181
+ if Log .debug_enabled (): Log .debug (f"convert({ into } , { obj } )" )
182
+
183
+ generic_def = get_origin (into )
184
+ if generic_def is not None and is_dataclass (generic_def ):
185
+ reified_types = {}
186
+ generic_type_args = get_args (into )
187
+ i = 0
188
+ for t in generic_def .__parameters__ :
189
+ reified_types [t ] = generic_type_args [i ]
190
+ i += 1
191
+ return convert (generic_def , obj , reified_types )
192
+
164
193
if is_dataclass (into ):
165
194
to = {}
166
195
for f in fields (into ):
167
- val = dict_get (f .name , obj )
168
- to [f .name ] = convert (f .type , val )
196
+ val = dict_get (f .name , obj )
197
+ # print(f"to[{f.name}] = convert({f.type}, {val}, {substitute_types})")
198
+ to [f .name ] = convert (f .type , val , substitute_types )
169
199
# print(f"to[{f.name}] = {to[f.name]}")
170
200
return into (** to )
171
201
elif is_list (into ):
172
- el_type = generic_arg (into )
202
+ el_type = _resolve_type ( generic_arg (into ), substitute_types )
173
203
to = []
174
204
for item in obj :
175
- to .append (convert (el_type , item ))
205
+ to .append (convert (el_type , item , substitute_types ))
176
206
return to
177
207
elif is_dict (into ):
178
208
key_type , val_type = generic_args (into )
209
+ key_type = _resolve_type (key_type , substitute_types )
210
+ val_type = _resolve_type (val_type , substitute_types )
179
211
to = {}
180
212
for key , val in obj .items ():
181
- to_key = convert (key_type , key )
182
- to_val = convert (val_type , val )
213
+ to_key = convert (key_type , key , substitute_types )
214
+ to_val = convert (val_type , val , substitute_types )
183
215
to [to_key ] = to_val
184
216
return to
185
217
else :
@@ -204,6 +236,7 @@ def convert(into:Type, obj:Any):
204
236
raise e
205
237
206
238
def from_json (into :Type , json_str :str ):
239
+ if json_str is None or json_str == "" : return None
207
240
json_obj = json .loads (json_str )
208
241
return convert (into , json_obj )
209
242
@@ -236,6 +269,7 @@ class WebServiceException(Exception):
236
269
inner_exception : Exception = None
237
270
response_status :ResponseStatus = None
238
271
272
+ T = TypeVar ('T' )
239
273
class JsonServiceClient :
240
274
base_url : str = None
241
275
reply_base_url : str = None
@@ -250,6 +284,8 @@ class JsonServiceClient:
250
284
request_filter :Callable [[SendContext ],None ] = None
251
285
global_response_filter :Callable [[Response ],None ] = None #static
252
286
response_filter :Callable [[Response ],None ] = None
287
+ exception_filter :Callable [[Response ,Exception ],None ] = None
288
+ global_exception_filter :Callable [[Response ,Exception ],None ] = None
253
289
254
290
def __init__ (self ,base_url ):
255
291
if not base_url :
@@ -286,29 +322,29 @@ def to_absolute_url(self, path_or_url:str):
286
322
return urljoin (self .base_url , path_or_url )
287
323
288
324
def get_url (self , path :str , response_as :Type , args :dict [str ,Any ]= None ):
289
- return self .send_url ("GET" , path , response_as , None , args )
325
+ return self .send_url (path , "GET" , response_as , None , args )
290
326
def delete_url (self , path :str , response_as :Type , args :dict [str ,Any ]= None ):
291
- return self .send_url ("DELETE" , path , response_as , None , args )
327
+ return self .send_url (path , "DELETE" , response_as , None , args )
292
328
def options_url (self , path :str , response_as :Type , args :dict [str ,Any ]= None ):
293
- return self .send_url ("OPTIONS" , path , response_as , None , args )
329
+ return self .send_url (path , "OPTIONS" , response_as , None , args )
294
330
def head_url (self , path :str , response_as :Type , args :dict [str ,Any ]= None ):
295
- return self .send_url ("HEAD" , path , response_as , None , args )
331
+ return self .send_url (path , "HEAD" , response_as , None , args )
296
332
297
333
def post_url (self , path :str , body :Any = None , response_as :Type = None , args :dict [str ,Any ]= None ):
298
- return self .send_url ("POST" , path , response_as , body , args )
334
+ return self .send_url (path , "POST" , response_as , body , args )
299
335
def put_url (self , path :str , body :Any = None , response_as :Type = None , args :dict [str ,Any ]= None ):
300
- return self .send_url ("PUT" , path , response_as , body , args )
336
+ return self .send_url (path , "PUT" , response_as , body , args )
301
337
def patch_url (self , path :str , body :Any = None , response_as :Type = None , args :dict [str ,Any ]= None ):
302
- return self .send_url ("PATCH" , path , response_as , body , args )
338
+ return self .send_url (path , "PATCH" , response_as , body , args )
303
339
304
- def send_url (self , method :str , path :str , response_as :Type = None , body = None , args :dict [str ,Any ]= None ):
340
+ def send_url (self , path :str , method :str = None , response_as :Type = None , body = None , args :dict [str ,Any ]= None ):
305
341
306
342
if body and not response_as :
307
343
response_as = _resolve_response_type (body )
308
344
309
345
info = SendContext (
310
346
headers = self .headers ,
311
- method = method ,
347
+ method = method or resolve_httpmethod ( body ) ,
312
348
url = self .to_absolute_url (path ),
313
349
request = None ,
314
350
body = body ,
@@ -318,7 +354,7 @@ def send_url(self, method:str, path:str, response_as:Type=None, body=None, args:
318
354
319
355
return self .send_request (info )
320
356
321
- def send (self ,request ,method ,body = None ,args = None ):
357
+ def send (self ,request ,method = "POST" ,body = None ,args = None ):
322
358
if not isinstance (request , IReturn ) and not isinstance (request , IReturnVoid ):
323
359
raise TypeError (f"'{ nameof (request )} ' does not implement IReturn or IReturnVoid" )
324
360
@@ -336,6 +372,49 @@ def send(self,request,method,body=None,args=None):
336
372
args = args ,
337
373
response_as = response_as ))
338
374
375
+ def assert_valid_batch_request (self , requests :list ):
376
+ if not isinstance (requests , list ):
377
+ raise TypeError (f"'{ nameof (requests )} ' is not a List" )
378
+
379
+ if len (requests ) == 0 : return []
380
+
381
+ request = requests [0 ]
382
+ if not isinstance (request , IReturn ) and not isinstance (request , IReturnVoid ):
383
+ raise TypeError (f"'{ nameof (request )} ' does not implement IReturn or IReturnVoid" )
384
+
385
+ item_response_as = _resolve_response_type (request )
386
+ if item_response_as is None :
387
+ raise TypeError (f"Could not resolve Response Type for '{ nameof (request )} '" )
388
+ return (request , item_response_as )
389
+
390
+ def send_all (self ,requests :List [IReturn [T ]]):
391
+ request , item_response_as = self .assert_valid_batch_request (requests )
392
+ url = urljoin (self .reply_base_url , nameof (request ) + "[]" )
393
+
394
+ return self .send_request (SendContext (
395
+ headers = self .headers ,
396
+ method = "POST" ,
397
+ url = url ,
398
+ request = list (requests ),
399
+ body = None ,
400
+ body_string = None ,
401
+ args = None ,
402
+ response_as = list .__class_getitem__ (item_response_as )))
403
+
404
+ def send_all_oneway (self ,requests :list ):
405
+ request , item_response_as = self .assert_valid_batch_request (requests )
406
+ url = urljoin (self .oneway_base_url , nameof (request ) + "[]" )
407
+
408
+ self .send_request (SendContext (
409
+ headers = self .headers ,
410
+ method = "POST" ,
411
+ url = url ,
412
+ request = list (requests ),
413
+ body = None ,
414
+ body_string = None ,
415
+ args = None ,
416
+ response_as = list .__class_getitem__ (item_response_as )))
417
+
339
418
def _resend_request (self , info ):
340
419
if has_request_body (info .method ):
341
420
headers = info .headers .copy () if info .headers else []
@@ -377,10 +456,14 @@ def _create_response(self, response:Response, info:SendContext):
377
456
378
457
return res_dto
379
458
380
- def _raise_error (self , e :Exception ):
459
+ def _raise_error (self , res :Response , e :Exception ) -> Exception :
460
+ if self .exception_filter :
461
+ self .exception_filter (res ,e )
462
+ if JsonServiceClient .global_exception_filter :
463
+ JsonServiceClient .global_exception_filter (res ,e )
381
464
return e
382
465
383
- def _handle_error (self , hold_res :Response , e :Exception ):
466
+ def _handle_error (self , hold_res :Response , e :Exception ) -> Exception :
384
467
if type (e ) == WebServiceException :
385
468
raise self ._raise_error (e )
386
469
@@ -404,11 +487,12 @@ def _handle_error(self, hold_res:Response, e:Exception):
404
487
405
488
try :
406
489
error_response = from_json (EmptyResponse , res .text )
407
- web_ex .response_status = error_response .response_status
490
+ if not error_response is None :
491
+ web_ex .response_status = error_response .response_status
408
492
except Exception as ex :
409
493
Log .error (f"Could not deserialize error response { res .text } " , ex )
410
494
411
- raise web_ex
495
+ raise self . _raise_error ( res , web_ex )
412
496
413
497
def send_request (self , info :SendContext ):
414
498
try :
0 commit comments