Skip to content

Commit b33a4b8

Browse files
committed
port and implement all client_test.dart
1 parent 991717d commit b33a4b8

File tree

5 files changed

+387
-70
lines changed

5 files changed

+387
-70
lines changed

servicestack/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
'StringResponse',
5555
'StringsResponse',
5656
'AuditBase',
57+
'TypeConverters',
5758
'JsonServiceClient',
5859
'WebServiceException',
5960
'to_json',
@@ -77,6 +78,6 @@
7778
]
7879

7980
from .dtos import *
80-
from .clients import JsonServiceClient, WebServiceException, to_json, from_json, qsvalue, resolve_httpmethod
81+
from .clients import TypeConverters, JsonServiceClient, WebServiceException, to_json, from_json, qsvalue, resolve_httpmethod
8182
from .utils import *
8283
from .fields import *

servicestack/clients.py

Lines changed: 122 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime, date, timedelta
22
import json
33
from re import L
4+
from requests.api import request
45

56
from requests.models import Response
67
from requests.exceptions import HTTPError
@@ -10,7 +11,7 @@
1011
from servicestack.utils import *
1112
from servicestack.fields import *
1213

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
1415
from dataclasses import dataclass, field, fields, asdict, is_dataclass, Field
1516
from dataclasses_json import dataclass_json, LetterCase, Undefined, config, mm
1617
from urllib.parse import urljoin, urlencode, quote_plus
@@ -23,17 +24,7 @@
2324

2425
JSON_MIME_TYPE = "application/json"
2526

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):
3728
print("")
3829
for attr in dir(obj):
3930
print(f"obj.{attr} = {getattr(obj, attr)}")
@@ -43,7 +34,10 @@ def _resolve_response_type(request):
4334
if isinstance(request, IReturn):
4435
for cls in request.__orig_bases__:
4536
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
4741
if isinstance(request, IReturnVoid):
4842
return type(None)
4943
return None
@@ -116,6 +110,8 @@ def _json_encoder(obj:Any):
116110
return to_timespan(obj)
117111
if isinstance(obj, bytes):
118112
return base64.b64encode(obj).decode('ascii')
113+
if isinstance(obj, decimal.Decimal):
114+
return float(obj)
119115
raise TypeError(f"Unsupported Type in JSON encoding: {type(obj)}")
120116

121117
def to_json(obj:Any):
@@ -135,12 +131,28 @@ def register(type:Type, converter:Callable[[Any],Any]):
135131
}
136132

137133
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]
140152

141-
def generic_arg(cls:Type): return cls.__args__[0]
142-
def generic_args(cls:Type): return cls.__args__
143153
def unwrap(cls:Type):
154+
if type(cls) == ForwardRef:
155+
cls = _resolve_forwardref(cls)
144156
if is_optional(cls):
145157
return generic_arg(cls)
146158
return cls
@@ -158,28 +170,48 @@ def dict_get(name:str, obj:dict, case:Callable[[str],str] = None):
158170
return dict_get(name.rstrip('_'), obj, case)
159171
return None
160172

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):
162178
if obj is None: return None
163179
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+
164193
if is_dataclass(into):
165194
to = {}
166195
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)
169199
# print(f"to[{f.name}] = {to[f.name]}")
170200
return into(**to)
171201
elif is_list(into):
172-
el_type = generic_arg(into)
202+
el_type = _resolve_type(generic_arg(into), substitute_types)
173203
to = []
174204
for item in obj:
175-
to.append(convert(el_type, item))
205+
to.append(convert(el_type, item, substitute_types))
176206
return to
177207
elif is_dict(into):
178208
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)
179211
to = {}
180212
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)
183215
to[to_key] = to_val
184216
return to
185217
else:
@@ -204,6 +236,7 @@ def convert(into:Type, obj:Any):
204236
raise e
205237

206238
def from_json(into:Type, json_str:str):
239+
if json_str is None or json_str == "": return None
207240
json_obj = json.loads(json_str)
208241
return convert(into, json_obj)
209242

@@ -236,6 +269,7 @@ class WebServiceException(Exception):
236269
inner_exception: Exception = None
237270
response_status:ResponseStatus = None
238271

272+
T = TypeVar('T')
239273
class JsonServiceClient:
240274
base_url: str = None
241275
reply_base_url: str = None
@@ -250,6 +284,8 @@ class JsonServiceClient:
250284
request_filter:Callable[[SendContext],None] = None
251285
global_response_filter:Callable[[Response],None] = None #static
252286
response_filter:Callable[[Response],None] = None
287+
exception_filter:Callable[[Response,Exception],None] = None
288+
global_exception_filter:Callable[[Response,Exception],None] = None
253289

254290
def __init__(self,base_url):
255291
if not base_url:
@@ -286,29 +322,29 @@ def to_absolute_url(self, path_or_url:str):
286322
return urljoin(self.base_url, path_or_url)
287323

288324
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)
290326
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)
292328
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)
294330
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)
296332

297333
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)
299335
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)
301337
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)
303339

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):
305341

306342
if body and not response_as:
307343
response_as = _resolve_response_type(body)
308344

309345
info = SendContext(
310346
headers=self.headers,
311-
method=method,
347+
method=method or resolve_httpmethod(body),
312348
url=self.to_absolute_url(path),
313349
request=None,
314350
body=body,
@@ -318,7 +354,7 @@ def send_url(self, method:str, path:str, response_as:Type=None, body=None, args:
318354

319355
return self.send_request(info)
320356

321-
def send(self,request,method,body=None,args=None):
357+
def send(self,request,method="POST",body=None,args=None):
322358
if not isinstance(request, IReturn) and not isinstance(request, IReturnVoid):
323359
raise TypeError(f"'{nameof(request)}' does not implement IReturn or IReturnVoid")
324360

@@ -336,6 +372,49 @@ def send(self,request,method,body=None,args=None):
336372
args=args,
337373
response_as=response_as))
338374

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+
339418
def _resend_request(self, info):
340419
if has_request_body(info.method):
341420
headers = info.headers.copy() if info.headers else []
@@ -377,10 +456,14 @@ def _create_response(self, response:Response, info:SendContext):
377456

378457
return res_dto
379458

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)
381464
return e
382465

383-
def _handle_error(self, hold_res:Response, e:Exception):
466+
def _handle_error(self, hold_res:Response, e:Exception) -> Exception:
384467
if type(e) == WebServiceException:
385468
raise self._raise_error(e)
386469

@@ -404,11 +487,12 @@ def _handle_error(self, hold_res:Response, e:Exception):
404487

405488
try:
406489
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
408492
except Exception as ex:
409493
Log.error(f"Could not deserialize error response {res.text}", ex)
410494

411-
raise web_ex
495+
raise self._raise_error(res, web_ex)
412496

413497
def send_request(self, info:SendContext):
414498
try:

servicestack/utils.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from datetime import datetime, timezone, timedelta
1+
from datetime import date, datetime, timezone, timedelta
22
from typing import Optional
33
import base64
4+
import typing
45

56
def index_of(target:str, needle:str):
67
try:
@@ -122,6 +123,9 @@ def from_timespan(str:Optional[str]):
122123
# print(f"\n\ntimedelta({str})[{has_time}] = {hours}:{minutes}:{seconds}\n\n")
123124
return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, milliseconds=int(ms*1000))
124125

126+
_MIN_UTC_DATE = datetime.min.replace(tzinfo=timezone.utc)
127+
_MIN_EPOCH = _MIN_UTC_DATE.timestamp()
128+
_MAX_UTC_DATE = datetime.max.replace(tzinfo=timezone.utc)
125129

126130
def from_datetime(json_date:str):
127131
if json_date.startswith("/Date("):
@@ -133,7 +137,13 @@ def from_datetime(json_date:str):
133137
epoch_str = last_left_part(epoch_and_zone, '+')
134138
# print(f"epoch_str = {epoch_str}")
135139
epoch = int(epoch_str)
136-
return datetime.fromtimestamp(epoch/1000, timezone.utc)
140+
try:
141+
return datetime.fromtimestamp(epoch/1000, timezone.utc)
142+
except Exception as e:
143+
if epoch < _MIN_EPOCH:
144+
return _MIN_UTC_DATE
145+
else: return _MAX_UTC_DATE
146+
137147
return datetime.fromisoformat(json_date)
138148

139149
def to_bytearray(value:Optional[bytes]):

0 commit comments

Comments
 (0)