zillow-rapidapi-client 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zillow_rapidapi_client/__init__.py +18 -0
- zillow_rapidapi_client/_hooks/__init__.py +5 -0
- zillow_rapidapi_client/_hooks/registration.py +13 -0
- zillow_rapidapi_client/_hooks/sdkhooks.py +76 -0
- zillow_rapidapi_client/_hooks/types.py +106 -0
- zillow_rapidapi_client/_version.py +15 -0
- zillow_rapidapi_client/basesdk.py +358 -0
- zillow_rapidapi_client/httpclient.py +134 -0
- zillow_rapidapi_client/models/__init__.py +40 -0
- zillow_rapidapi_client/models/apierror.py +22 -0
- zillow_rapidapi_client/models/property.py +163 -0
- zillow_rapidapi_client/models/propertyextendedsearchop.py +106 -0
- zillow_rapidapi_client/models/propertysearchresponse.py +32 -0
- zillow_rapidapi_client/properties.py +221 -0
- zillow_rapidapi_client/py.typed +1 -0
- zillow_rapidapi_client/sdk.py +114 -0
- zillow_rapidapi_client/sdkconfiguration.py +52 -0
- zillow_rapidapi_client/types/__init__.py +21 -0
- zillow_rapidapi_client/types/basemodel.py +39 -0
- zillow_rapidapi_client/utils/__init__.py +99 -0
- zillow_rapidapi_client/utils/annotations.py +55 -0
- zillow_rapidapi_client/utils/enums.py +34 -0
- zillow_rapidapi_client/utils/eventstreaming.py +238 -0
- zillow_rapidapi_client/utils/forms.py +202 -0
- zillow_rapidapi_client/utils/headers.py +136 -0
- zillow_rapidapi_client/utils/logger.py +27 -0
- zillow_rapidapi_client/utils/metadata.py +118 -0
- zillow_rapidapi_client/utils/queryparams.py +205 -0
- zillow_rapidapi_client/utils/requestbodies.py +66 -0
- zillow_rapidapi_client/utils/retries.py +217 -0
- zillow_rapidapi_client/utils/security.py +174 -0
- zillow_rapidapi_client/utils/serializers.py +215 -0
- zillow_rapidapi_client/utils/url.py +155 -0
- zillow_rapidapi_client/utils/values.py +137 -0
- zillow_rapidapi_client-0.1.3.dist-info/METADATA +419 -0
- zillow_rapidapi_client-0.1.3.dist-info/RECORD +37 -0
- zillow_rapidapi_client-0.1.3.dist-info/WHEEL +4 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
|
2
|
+
|
3
|
+
from typing import Optional, Type, TypeVar, Union
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from pydantic.fields import FieldInfo
|
6
|
+
|
7
|
+
|
8
|
+
T = TypeVar("T")
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class SecurityMetadata:
|
13
|
+
option: bool = False
|
14
|
+
scheme: bool = False
|
15
|
+
scheme_type: Optional[str] = None
|
16
|
+
sub_type: Optional[str] = None
|
17
|
+
field_name: Optional[str] = None
|
18
|
+
|
19
|
+
def get_field_name(self, default: str) -> str:
|
20
|
+
return self.field_name or default
|
21
|
+
|
22
|
+
|
23
|
+
@dataclass
|
24
|
+
class ParamMetadata:
|
25
|
+
serialization: Optional[str] = None
|
26
|
+
style: str = "simple"
|
27
|
+
explode: bool = False
|
28
|
+
|
29
|
+
|
30
|
+
@dataclass
|
31
|
+
class PathParamMetadata(ParamMetadata):
|
32
|
+
pass
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass
|
36
|
+
class QueryParamMetadata(ParamMetadata):
|
37
|
+
style: str = "form"
|
38
|
+
explode: bool = True
|
39
|
+
|
40
|
+
|
41
|
+
@dataclass
|
42
|
+
class HeaderMetadata(ParamMetadata):
|
43
|
+
pass
|
44
|
+
|
45
|
+
|
46
|
+
@dataclass
|
47
|
+
class RequestMetadata:
|
48
|
+
media_type: str = "application/octet-stream"
|
49
|
+
|
50
|
+
|
51
|
+
@dataclass
|
52
|
+
class MultipartFormMetadata:
|
53
|
+
file: bool = False
|
54
|
+
content: bool = False
|
55
|
+
json: bool = False
|
56
|
+
|
57
|
+
|
58
|
+
@dataclass
|
59
|
+
class FormMetadata:
|
60
|
+
json: bool = False
|
61
|
+
style: str = "form"
|
62
|
+
explode: bool = True
|
63
|
+
|
64
|
+
|
65
|
+
class FieldMetadata:
|
66
|
+
security: Optional[SecurityMetadata] = None
|
67
|
+
path: Optional[PathParamMetadata] = None
|
68
|
+
query: Optional[QueryParamMetadata] = None
|
69
|
+
header: Optional[HeaderMetadata] = None
|
70
|
+
request: Optional[RequestMetadata] = None
|
71
|
+
form: Optional[FormMetadata] = None
|
72
|
+
multipart: Optional[MultipartFormMetadata] = None
|
73
|
+
|
74
|
+
def __init__(
|
75
|
+
self,
|
76
|
+
security: Optional[SecurityMetadata] = None,
|
77
|
+
path: Optional[Union[PathParamMetadata, bool]] = None,
|
78
|
+
query: Optional[Union[QueryParamMetadata, bool]] = None,
|
79
|
+
header: Optional[Union[HeaderMetadata, bool]] = None,
|
80
|
+
request: Optional[Union[RequestMetadata, bool]] = None,
|
81
|
+
form: Optional[Union[FormMetadata, bool]] = None,
|
82
|
+
multipart: Optional[Union[MultipartFormMetadata, bool]] = None,
|
83
|
+
):
|
84
|
+
self.security = security
|
85
|
+
self.path = PathParamMetadata() if isinstance(path, bool) else path
|
86
|
+
self.query = QueryParamMetadata() if isinstance(query, bool) else query
|
87
|
+
self.header = HeaderMetadata() if isinstance(header, bool) else header
|
88
|
+
self.request = RequestMetadata() if isinstance(request, bool) else request
|
89
|
+
self.form = FormMetadata() if isinstance(form, bool) else form
|
90
|
+
self.multipart = (
|
91
|
+
MultipartFormMetadata() if isinstance(multipart, bool) else multipart
|
92
|
+
)
|
93
|
+
|
94
|
+
|
95
|
+
def find_field_metadata(field_info: FieldInfo, metadata_type: Type[T]) -> Optional[T]:
|
96
|
+
metadata = find_metadata(field_info, FieldMetadata)
|
97
|
+
if not metadata:
|
98
|
+
return None
|
99
|
+
|
100
|
+
fields = metadata.__dict__
|
101
|
+
|
102
|
+
for field in fields:
|
103
|
+
if isinstance(fields[field], metadata_type):
|
104
|
+
return fields[field]
|
105
|
+
|
106
|
+
return None
|
107
|
+
|
108
|
+
|
109
|
+
def find_metadata(field_info: FieldInfo, metadata_type: Type[T]) -> Optional[T]:
|
110
|
+
metadata = field_info.metadata
|
111
|
+
if not metadata:
|
112
|
+
return None
|
113
|
+
|
114
|
+
for md in metadata:
|
115
|
+
if isinstance(md, metadata_type):
|
116
|
+
return md
|
117
|
+
|
118
|
+
return None
|
@@ -0,0 +1,205 @@
|
|
1
|
+
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
|
2
|
+
|
3
|
+
from typing import (
|
4
|
+
Any,
|
5
|
+
Dict,
|
6
|
+
get_type_hints,
|
7
|
+
List,
|
8
|
+
Optional,
|
9
|
+
)
|
10
|
+
|
11
|
+
from pydantic import BaseModel
|
12
|
+
from pydantic.fields import FieldInfo
|
13
|
+
|
14
|
+
from .metadata import (
|
15
|
+
QueryParamMetadata,
|
16
|
+
find_field_metadata,
|
17
|
+
)
|
18
|
+
from .values import (
|
19
|
+
_get_serialized_params,
|
20
|
+
_is_set,
|
21
|
+
_populate_from_globals,
|
22
|
+
_val_to_string,
|
23
|
+
)
|
24
|
+
from .forms import _populate_form
|
25
|
+
|
26
|
+
|
27
|
+
def get_query_params(
|
28
|
+
query_params: Any,
|
29
|
+
gbls: Optional[Any] = None,
|
30
|
+
) -> Dict[str, List[str]]:
|
31
|
+
params: Dict[str, List[str]] = {}
|
32
|
+
|
33
|
+
globals_already_populated = _populate_query_params(query_params, gbls, params, [])
|
34
|
+
if _is_set(gbls):
|
35
|
+
_populate_query_params(gbls, None, params, globals_already_populated)
|
36
|
+
|
37
|
+
return params
|
38
|
+
|
39
|
+
|
40
|
+
def _populate_query_params(
|
41
|
+
query_params: Any,
|
42
|
+
gbls: Any,
|
43
|
+
query_param_values: Dict[str, List[str]],
|
44
|
+
skip_fields: List[str],
|
45
|
+
) -> List[str]:
|
46
|
+
globals_already_populated: List[str] = []
|
47
|
+
|
48
|
+
if not isinstance(query_params, BaseModel):
|
49
|
+
return globals_already_populated
|
50
|
+
|
51
|
+
param_fields: Dict[str, FieldInfo] = query_params.__class__.model_fields
|
52
|
+
param_field_types = get_type_hints(query_params.__class__)
|
53
|
+
for name in param_fields:
|
54
|
+
if name in skip_fields:
|
55
|
+
continue
|
56
|
+
|
57
|
+
field = param_fields[name]
|
58
|
+
|
59
|
+
metadata = find_field_metadata(field, QueryParamMetadata)
|
60
|
+
if not metadata:
|
61
|
+
continue
|
62
|
+
|
63
|
+
value = getattr(query_params, name) if _is_set(query_params) else None
|
64
|
+
|
65
|
+
value, global_found = _populate_from_globals(
|
66
|
+
name, value, QueryParamMetadata, gbls
|
67
|
+
)
|
68
|
+
if global_found:
|
69
|
+
globals_already_populated.append(name)
|
70
|
+
|
71
|
+
f_name = field.alias if field.alias is not None else name
|
72
|
+
serialization = metadata.serialization
|
73
|
+
if serialization is not None:
|
74
|
+
serialized_parms = _get_serialized_params(
|
75
|
+
metadata, f_name, value, param_field_types[name]
|
76
|
+
)
|
77
|
+
for key, value in serialized_parms.items():
|
78
|
+
if key in query_param_values:
|
79
|
+
query_param_values[key].extend(value)
|
80
|
+
else:
|
81
|
+
query_param_values[key] = [value]
|
82
|
+
else:
|
83
|
+
style = metadata.style
|
84
|
+
if style == "deepObject":
|
85
|
+
_populate_deep_object_query_params(f_name, value, query_param_values)
|
86
|
+
elif style == "form":
|
87
|
+
_populate_delimited_query_params(
|
88
|
+
metadata, f_name, value, ",", query_param_values
|
89
|
+
)
|
90
|
+
elif style == "pipeDelimited":
|
91
|
+
_populate_delimited_query_params(
|
92
|
+
metadata, f_name, value, "|", query_param_values
|
93
|
+
)
|
94
|
+
else:
|
95
|
+
raise NotImplementedError(
|
96
|
+
f"query param style {style} not yet supported"
|
97
|
+
)
|
98
|
+
|
99
|
+
return globals_already_populated
|
100
|
+
|
101
|
+
|
102
|
+
def _populate_deep_object_query_params(
|
103
|
+
field_name: str,
|
104
|
+
obj: Any,
|
105
|
+
params: Dict[str, List[str]],
|
106
|
+
):
|
107
|
+
if not _is_set(obj):
|
108
|
+
return
|
109
|
+
|
110
|
+
if isinstance(obj, BaseModel):
|
111
|
+
_populate_deep_object_query_params_basemodel(field_name, obj, params)
|
112
|
+
elif isinstance(obj, Dict):
|
113
|
+
_populate_deep_object_query_params_dict(field_name, obj, params)
|
114
|
+
|
115
|
+
|
116
|
+
def _populate_deep_object_query_params_basemodel(
|
117
|
+
prior_params_key: str,
|
118
|
+
obj: Any,
|
119
|
+
params: Dict[str, List[str]],
|
120
|
+
):
|
121
|
+
if not _is_set(obj) or not isinstance(obj, BaseModel):
|
122
|
+
return
|
123
|
+
|
124
|
+
obj_fields: Dict[str, FieldInfo] = obj.__class__.model_fields
|
125
|
+
for name in obj_fields:
|
126
|
+
obj_field = obj_fields[name]
|
127
|
+
|
128
|
+
f_name = obj_field.alias if obj_field.alias is not None else name
|
129
|
+
|
130
|
+
params_key = f"{prior_params_key}[{f_name}]"
|
131
|
+
|
132
|
+
obj_param_metadata = find_field_metadata(obj_field, QueryParamMetadata)
|
133
|
+
if not _is_set(obj_param_metadata):
|
134
|
+
continue
|
135
|
+
|
136
|
+
obj_val = getattr(obj, name)
|
137
|
+
if not _is_set(obj_val):
|
138
|
+
continue
|
139
|
+
|
140
|
+
if isinstance(obj_val, BaseModel):
|
141
|
+
_populate_deep_object_query_params_basemodel(params_key, obj_val, params)
|
142
|
+
elif isinstance(obj_val, Dict):
|
143
|
+
_populate_deep_object_query_params_dict(params_key, obj_val, params)
|
144
|
+
elif isinstance(obj_val, List):
|
145
|
+
_populate_deep_object_query_params_list(params_key, obj_val, params)
|
146
|
+
else:
|
147
|
+
params[params_key] = [_val_to_string(obj_val)]
|
148
|
+
|
149
|
+
|
150
|
+
def _populate_deep_object_query_params_dict(
|
151
|
+
prior_params_key: str,
|
152
|
+
value: Dict,
|
153
|
+
params: Dict[str, List[str]],
|
154
|
+
):
|
155
|
+
if not _is_set(value):
|
156
|
+
return
|
157
|
+
|
158
|
+
for key, val in value.items():
|
159
|
+
if not _is_set(val):
|
160
|
+
continue
|
161
|
+
|
162
|
+
params_key = f"{prior_params_key}[{key}]"
|
163
|
+
|
164
|
+
if isinstance(val, BaseModel):
|
165
|
+
_populate_deep_object_query_params_basemodel(params_key, val, params)
|
166
|
+
elif isinstance(val, Dict):
|
167
|
+
_populate_deep_object_query_params_dict(params_key, val, params)
|
168
|
+
elif isinstance(val, List):
|
169
|
+
_populate_deep_object_query_params_list(params_key, val, params)
|
170
|
+
else:
|
171
|
+
params[params_key] = [_val_to_string(val)]
|
172
|
+
|
173
|
+
|
174
|
+
def _populate_deep_object_query_params_list(
|
175
|
+
params_key: str,
|
176
|
+
value: List,
|
177
|
+
params: Dict[str, List[str]],
|
178
|
+
):
|
179
|
+
if not _is_set(value):
|
180
|
+
return
|
181
|
+
|
182
|
+
for val in value:
|
183
|
+
if not _is_set(val):
|
184
|
+
continue
|
185
|
+
|
186
|
+
if params.get(params_key) is None:
|
187
|
+
params[params_key] = []
|
188
|
+
|
189
|
+
params[params_key].append(_val_to_string(val))
|
190
|
+
|
191
|
+
|
192
|
+
def _populate_delimited_query_params(
|
193
|
+
metadata: QueryParamMetadata,
|
194
|
+
field_name: str,
|
195
|
+
obj: Any,
|
196
|
+
delimiter: str,
|
197
|
+
query_param_values: Dict[str, List[str]],
|
198
|
+
):
|
199
|
+
_populate_form(
|
200
|
+
field_name,
|
201
|
+
metadata.explode,
|
202
|
+
obj,
|
203
|
+
delimiter,
|
204
|
+
query_param_values,
|
205
|
+
)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
|
2
|
+
|
3
|
+
import io
|
4
|
+
from dataclasses import dataclass
|
5
|
+
import re
|
6
|
+
from typing import (
|
7
|
+
Any,
|
8
|
+
Optional,
|
9
|
+
)
|
10
|
+
|
11
|
+
from .forms import serialize_form_data, serialize_multipart_form
|
12
|
+
|
13
|
+
from .serializers import marshal_json
|
14
|
+
|
15
|
+
SERIALIZATION_METHOD_TO_CONTENT_TYPE = {
|
16
|
+
"json": "application/json",
|
17
|
+
"form": "application/x-www-form-urlencoded",
|
18
|
+
"multipart": "multipart/form-data",
|
19
|
+
"raw": "application/octet-stream",
|
20
|
+
"string": "text/plain",
|
21
|
+
}
|
22
|
+
|
23
|
+
|
24
|
+
@dataclass
|
25
|
+
class SerializedRequestBody:
|
26
|
+
media_type: Optional[str] = None
|
27
|
+
content: Optional[Any] = None
|
28
|
+
data: Optional[Any] = None
|
29
|
+
files: Optional[Any] = None
|
30
|
+
|
31
|
+
|
32
|
+
def serialize_request_body(
|
33
|
+
request_body: Any,
|
34
|
+
nullable: bool,
|
35
|
+
optional: bool,
|
36
|
+
serialization_method: str,
|
37
|
+
request_body_type,
|
38
|
+
) -> Optional[SerializedRequestBody]:
|
39
|
+
if request_body is None:
|
40
|
+
if not nullable and optional:
|
41
|
+
return None
|
42
|
+
|
43
|
+
media_type = SERIALIZATION_METHOD_TO_CONTENT_TYPE[serialization_method]
|
44
|
+
|
45
|
+
serialized_request_body = SerializedRequestBody(media_type)
|
46
|
+
|
47
|
+
if re.match(r"(application|text)\/.*?\+*json.*", media_type) is not None:
|
48
|
+
serialized_request_body.content = marshal_json(request_body, request_body_type)
|
49
|
+
elif re.match(r"multipart\/.*", media_type) is not None:
|
50
|
+
(
|
51
|
+
serialized_request_body.media_type,
|
52
|
+
serialized_request_body.data,
|
53
|
+
serialized_request_body.files,
|
54
|
+
) = serialize_multipart_form(media_type, request_body)
|
55
|
+
elif re.match(r"application\/x-www-form-urlencoded.*", media_type) is not None:
|
56
|
+
serialized_request_body.data = serialize_form_data(request_body)
|
57
|
+
elif isinstance(request_body, (bytes, bytearray, io.BytesIO, io.BufferedReader)):
|
58
|
+
serialized_request_body.content = request_body
|
59
|
+
elif isinstance(request_body, str):
|
60
|
+
serialized_request_body.content = request_body
|
61
|
+
else:
|
62
|
+
raise TypeError(
|
63
|
+
f"invalid request body type {type(request_body)} for mediaType {media_type}"
|
64
|
+
)
|
65
|
+
|
66
|
+
return serialized_request_body
|
@@ -0,0 +1,217 @@
|
|
1
|
+
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import random
|
5
|
+
import time
|
6
|
+
from typing import List
|
7
|
+
|
8
|
+
import httpx
|
9
|
+
|
10
|
+
|
11
|
+
class BackoffStrategy:
|
12
|
+
initial_interval: int
|
13
|
+
max_interval: int
|
14
|
+
exponent: float
|
15
|
+
max_elapsed_time: int
|
16
|
+
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
initial_interval: int,
|
20
|
+
max_interval: int,
|
21
|
+
exponent: float,
|
22
|
+
max_elapsed_time: int,
|
23
|
+
):
|
24
|
+
self.initial_interval = initial_interval
|
25
|
+
self.max_interval = max_interval
|
26
|
+
self.exponent = exponent
|
27
|
+
self.max_elapsed_time = max_elapsed_time
|
28
|
+
|
29
|
+
|
30
|
+
class RetryConfig:
|
31
|
+
strategy: str
|
32
|
+
backoff: BackoffStrategy
|
33
|
+
retry_connection_errors: bool
|
34
|
+
|
35
|
+
def __init__(
|
36
|
+
self, strategy: str, backoff: BackoffStrategy, retry_connection_errors: bool
|
37
|
+
):
|
38
|
+
self.strategy = strategy
|
39
|
+
self.backoff = backoff
|
40
|
+
self.retry_connection_errors = retry_connection_errors
|
41
|
+
|
42
|
+
|
43
|
+
class Retries:
|
44
|
+
config: RetryConfig
|
45
|
+
status_codes: List[str]
|
46
|
+
|
47
|
+
def __init__(self, config: RetryConfig, status_codes: List[str]):
|
48
|
+
self.config = config
|
49
|
+
self.status_codes = status_codes
|
50
|
+
|
51
|
+
|
52
|
+
class TemporaryError(Exception):
|
53
|
+
response: httpx.Response
|
54
|
+
|
55
|
+
def __init__(self, response: httpx.Response):
|
56
|
+
self.response = response
|
57
|
+
|
58
|
+
|
59
|
+
class PermanentError(Exception):
|
60
|
+
inner: Exception
|
61
|
+
|
62
|
+
def __init__(self, inner: Exception):
|
63
|
+
self.inner = inner
|
64
|
+
|
65
|
+
|
66
|
+
def retry(func, retries: Retries):
|
67
|
+
if retries.config.strategy == "backoff":
|
68
|
+
|
69
|
+
def do_request() -> httpx.Response:
|
70
|
+
res: httpx.Response
|
71
|
+
try:
|
72
|
+
res = func()
|
73
|
+
|
74
|
+
for code in retries.status_codes:
|
75
|
+
if "X" in code.upper():
|
76
|
+
code_range = int(code[0])
|
77
|
+
|
78
|
+
status_major = res.status_code / 100
|
79
|
+
|
80
|
+
if code_range <= status_major < code_range + 1:
|
81
|
+
raise TemporaryError(res)
|
82
|
+
else:
|
83
|
+
parsed_code = int(code)
|
84
|
+
|
85
|
+
if res.status_code == parsed_code:
|
86
|
+
raise TemporaryError(res)
|
87
|
+
except httpx.ConnectError as exception:
|
88
|
+
if retries.config.retry_connection_errors:
|
89
|
+
raise
|
90
|
+
|
91
|
+
raise PermanentError(exception) from exception
|
92
|
+
except httpx.TimeoutException as exception:
|
93
|
+
if retries.config.retry_connection_errors:
|
94
|
+
raise
|
95
|
+
|
96
|
+
raise PermanentError(exception) from exception
|
97
|
+
except TemporaryError:
|
98
|
+
raise
|
99
|
+
except Exception as exception:
|
100
|
+
raise PermanentError(exception) from exception
|
101
|
+
|
102
|
+
return res
|
103
|
+
|
104
|
+
return retry_with_backoff(
|
105
|
+
do_request,
|
106
|
+
retries.config.backoff.initial_interval,
|
107
|
+
retries.config.backoff.max_interval,
|
108
|
+
retries.config.backoff.exponent,
|
109
|
+
retries.config.backoff.max_elapsed_time,
|
110
|
+
)
|
111
|
+
|
112
|
+
return func()
|
113
|
+
|
114
|
+
|
115
|
+
async def retry_async(func, retries: Retries):
|
116
|
+
if retries.config.strategy == "backoff":
|
117
|
+
|
118
|
+
async def do_request() -> httpx.Response:
|
119
|
+
res: httpx.Response
|
120
|
+
try:
|
121
|
+
res = await func()
|
122
|
+
|
123
|
+
for code in retries.status_codes:
|
124
|
+
if "X" in code.upper():
|
125
|
+
code_range = int(code[0])
|
126
|
+
|
127
|
+
status_major = res.status_code / 100
|
128
|
+
|
129
|
+
if code_range <= status_major < code_range + 1:
|
130
|
+
raise TemporaryError(res)
|
131
|
+
else:
|
132
|
+
parsed_code = int(code)
|
133
|
+
|
134
|
+
if res.status_code == parsed_code:
|
135
|
+
raise TemporaryError(res)
|
136
|
+
except httpx.ConnectError as exception:
|
137
|
+
if retries.config.retry_connection_errors:
|
138
|
+
raise
|
139
|
+
|
140
|
+
raise PermanentError(exception) from exception
|
141
|
+
except httpx.TimeoutException as exception:
|
142
|
+
if retries.config.retry_connection_errors:
|
143
|
+
raise
|
144
|
+
|
145
|
+
raise PermanentError(exception) from exception
|
146
|
+
except TemporaryError:
|
147
|
+
raise
|
148
|
+
except Exception as exception:
|
149
|
+
raise PermanentError(exception) from exception
|
150
|
+
|
151
|
+
return res
|
152
|
+
|
153
|
+
return await retry_with_backoff_async(
|
154
|
+
do_request,
|
155
|
+
retries.config.backoff.initial_interval,
|
156
|
+
retries.config.backoff.max_interval,
|
157
|
+
retries.config.backoff.exponent,
|
158
|
+
retries.config.backoff.max_elapsed_time,
|
159
|
+
)
|
160
|
+
|
161
|
+
return await func()
|
162
|
+
|
163
|
+
|
164
|
+
def retry_with_backoff(
|
165
|
+
func,
|
166
|
+
initial_interval=500,
|
167
|
+
max_interval=60000,
|
168
|
+
exponent=1.5,
|
169
|
+
max_elapsed_time=3600000,
|
170
|
+
):
|
171
|
+
start = round(time.time() * 1000)
|
172
|
+
retries = 0
|
173
|
+
|
174
|
+
while True:
|
175
|
+
try:
|
176
|
+
return func()
|
177
|
+
except PermanentError as exception:
|
178
|
+
raise exception.inner
|
179
|
+
except Exception as exception: # pylint: disable=broad-exception-caught
|
180
|
+
now = round(time.time() * 1000)
|
181
|
+
if now - start > max_elapsed_time:
|
182
|
+
if isinstance(exception, TemporaryError):
|
183
|
+
return exception.response
|
184
|
+
|
185
|
+
raise
|
186
|
+
sleep = (initial_interval / 1000) * exponent**retries + random.uniform(0, 1)
|
187
|
+
sleep = min(sleep, max_interval / 1000)
|
188
|
+
time.sleep(sleep)
|
189
|
+
retries += 1
|
190
|
+
|
191
|
+
|
192
|
+
async def retry_with_backoff_async(
|
193
|
+
func,
|
194
|
+
initial_interval=500,
|
195
|
+
max_interval=60000,
|
196
|
+
exponent=1.5,
|
197
|
+
max_elapsed_time=3600000,
|
198
|
+
):
|
199
|
+
start = round(time.time() * 1000)
|
200
|
+
retries = 0
|
201
|
+
|
202
|
+
while True:
|
203
|
+
try:
|
204
|
+
return await func()
|
205
|
+
except PermanentError as exception:
|
206
|
+
raise exception.inner
|
207
|
+
except Exception as exception: # pylint: disable=broad-exception-caught
|
208
|
+
now = round(time.time() * 1000)
|
209
|
+
if now - start > max_elapsed_time:
|
210
|
+
if isinstance(exception, TemporaryError):
|
211
|
+
return exception.response
|
212
|
+
|
213
|
+
raise
|
214
|
+
sleep = (initial_interval / 1000) * exponent**retries + random.uniform(0, 1)
|
215
|
+
sleep = min(sleep, max_interval / 1000)
|
216
|
+
await asyncio.sleep(sleep)
|
217
|
+
retries += 1
|